From d9450e823ee59c2b7eefcb3480acc2ad7b353179 Mon Sep 17 00:00:00 2001 From: zenix Date: Thu, 3 Feb 2022 20:19:56 +0900 Subject: [PATCH] fix all the fixedpoint use other than strategy --- pkg/backtest/matching.go | 103 ++++++++++++++--------------- pkg/backtest/priceorder.go | 6 +- pkg/bbgo/environment.go | 2 +- pkg/bbgo/interact.go | 13 ++-- pkg/bbgo/order_execution.go | 101 ++++++++++++++-------------- pkg/bbgo/order_processor.go | 24 +++---- pkg/bbgo/order_store.go | 2 +- pkg/bbgo/profitstats.go | 114 ++++++++++++++++---------------- pkg/bbgo/quantity_amount.go | 6 +- pkg/bbgo/quota.go | 14 ++-- pkg/bbgo/session.go | 20 +++--- pkg/bbgo/smart_stops.go | 72 ++++++++++---------- pkg/bbgo/twap_order_executor.go | 85 ++++++++++++------------ pkg/server/routes.go | 22 +++--- 14 files changed, 295 insertions(+), 289 deletions(-) diff --git a/pkg/backtest/matching.go b/pkg/backtest/matching.go index d77ca6175..ecf8a86af 100644 --- a/pkg/backtest/matching.go +++ b/pkg/backtest/matching.go @@ -16,7 +16,7 @@ import ( // BINANCE uses 0.1% 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 -const DefaultFeeRate = 0.075 * 0.01 +var DefaultFeeRate = fixedpoint.NewFromFloat(0.075 * 0.01) var orderID uint64 = 1 var tradeID uint64 = 1 @@ -92,12 +92,12 @@ func (m *SimplePriceMatching) CancelOrder(o types.Order) (types.Order, error) { switch o.Side { case types.SideTypeBuy: - if err := m.Account.UnlockBalance(m.Market.QuoteCurrency, fixedpoint.NewFromFloat(o.Price*o.Quantity)); err != nil { + if err := m.Account.UnlockBalance(m.Market.QuoteCurrency, o.Price.Mul(o.Quantity)); err != nil { return o, err } case types.SideTypeSell: - if err := m.Account.UnlockBalance(m.Market.BaseCurrency, fixedpoint.NewFromFloat(o.Quantity)); err != nil { + if err := m.Account.UnlockBalance(m.Market.BaseCurrency, o.Quantity); err != nil { return o, err } } @@ -114,32 +114,32 @@ func (m *SimplePriceMatching) PlaceOrder(o types.SubmitOrder) (closedOrders *typ switch o.Type { case types.OrderTypeMarket: - if m.LastPrice == 0 { + if m.LastPrice.IsZero() { panic("unexpected: last price can not be zero") } - price = m.LastPrice.Float64() + price = m.LastPrice case types.OrderTypeLimit, types.OrderTypeLimitMaker: price = o.Price } - if o.Quantity < m.Market.MinQuantity { - return nil, nil, fmt.Errorf("order quantity %f is less than minQuantity %f, order: %+v", o.Quantity, m.Market.MinQuantity, o) + if o.Quantity.Compare(m.Market.MinQuantity) < 0 { + return nil, nil, fmt.Errorf("order quantity %s is less than minQuantity %s, order: %+v", o.Quantity.String(), m.Market.MinQuantity.String(), o) } - quoteQuantity := o.Quantity * price - if quoteQuantity < m.Market.MinNotional { - return nil, nil, fmt.Errorf("order amount %f is less than minNotional %f, order: %+v", quoteQuantity, m.Market.MinNotional, o) + quoteQuantity := o.Quantity.Mul(price) + if quoteQuantity.Compare(m.Market.MinNotional) < 0 { + return nil, nil, fmt.Errorf("order amount %s is less than minNotional %s, order: %+v", quoteQuantity.String(), m.Market.MinNotional.String(), o) } switch o.Side { case types.SideTypeBuy: - if err := m.Account.LockBalance(m.Market.QuoteCurrency, fixedpoint.NewFromFloat(quoteQuantity)); err != nil { + if err := m.Account.LockBalance(m.Market.QuoteCurrency, quoteQuantity); err != nil { return nil, nil, err } case types.SideTypeSell: - if err := m.Account.LockBalance(m.Market.BaseCurrency, fixedpoint.NewFromFloat(o.Quantity)); err != nil { + if err := m.Account.LockBalance(m.Market.BaseCurrency, o.Quantity); err != nil { return nil, nil, err } } @@ -190,13 +190,13 @@ func (m *SimplePriceMatching) executeTrade(trade types.Trade) { var err error // execute trade, update account balances if trade.IsBuyer { - err = m.Account.UseLockedBalance(m.Market.QuoteCurrency, fixedpoint.NewFromFloat(trade.Price*trade.Quantity)) + err = m.Account.UseLockedBalance(m.Market.QuoteCurrency, trade.Price.Mul(trade.Quantity)) - m.Account.AddBalance(m.Market.BaseCurrency, fixedpoint.NewFromFloat(trade.Quantity)) + m.Account.AddBalance(m.Market.BaseCurrency, trade.Quantity) } else { - err = m.Account.UseLockedBalance(m.Market.BaseCurrency, fixedpoint.NewFromFloat(trade.Quantity)) + err = m.Account.UseLockedBalance(m.Market.BaseCurrency, trade.Quantity) - m.Account.AddBalance(m.Market.QuoteCurrency, fixedpoint.NewFromFloat(trade.Quantity*trade.Price)) + m.Account.AddBalance(m.Market.QuoteCurrency, trade.Quantity.Mul(trade.Price)) } if err != nil { @@ -213,37 +213,37 @@ func (m *SimplePriceMatching) newTradeFromOrder(order types.Order, isMaker bool) // MAX uses 0.050% for maker and 0.15% for taker var feeRate = DefaultFeeRate if isMaker { - if m.MakerFeeRate > 0 { - feeRate = m.MakerFeeRate.Float64() + if m.MakerFeeRate.Sign() > 0 { + feeRate = m.MakerFeeRate } } else { - if m.TakerFeeRate > 0 { - feeRate = m.TakerFeeRate.Float64() + if m.TakerFeeRate.Sign() > 0 { + feeRate = m.TakerFeeRate } } price := order.Price switch order.Type { case types.OrderTypeMarket, types.OrderTypeStopMarket: - if m.LastPrice == 0 { + if m.LastPrice.IsZero() { panic("unexpected: last price can not be zero") } - price = m.LastPrice.Float64() + price = m.LastPrice } - var fee float64 + var fee fixedpoint.Value var feeCurrency string switch order.Side { case types.SideTypeBuy: - fee = order.Quantity * feeRate + fee = order.Quantity.Mul(feeRate) feeCurrency = m.Market.BaseCurrency case types.SideTypeSell: - fee = order.Quantity * price * feeRate + fee = order.Quantity.Mul(price).Mul(feeRate) feeCurrency = m.Market.QuoteCurrency } @@ -255,7 +255,7 @@ func (m *SimplePriceMatching) newTradeFromOrder(order types.Order, isMaker bool) Exchange: "backtest", Price: price, Quantity: order.Quantity, - QuoteQuantity: order.Quantity * price, + QuoteQuantity: order.Quantity.Mul(price), Symbol: order.Symbol, Side: order.Side, IsBuyer: order.Side == types.SideTypeBuy, @@ -267,7 +267,6 @@ func (m *SimplePriceMatching) newTradeFromOrder(order types.Order, isMaker bool) } func (m *SimplePriceMatching) BuyToPrice(price fixedpoint.Value) (closedOrders []types.Order, trades []types.Trade) { - var priceF = price.Float64() var askOrders []types.Order for _, o := range m.askOrders { @@ -275,7 +274,7 @@ func (m *SimplePriceMatching) BuyToPrice(price fixedpoint.Value) (closedOrders [ case types.OrderTypeStopMarket: // should we trigger the order - if priceF <= o.StopPrice { + if price.Compare(o.StopPrice) <= 0 { // not triggering it, put it back askOrders = append(askOrders, o) break @@ -283,7 +282,7 @@ func (m *SimplePriceMatching) BuyToPrice(price fixedpoint.Value) (closedOrders [ o.Type = types.OrderTypeMarket o.ExecutedQuantity = o.Quantity - o.Price = priceF + o.Price = price o.Status = types.OrderStatusFilled closedOrders = append(closedOrders, o) @@ -296,7 +295,7 @@ func (m *SimplePriceMatching) BuyToPrice(price fixedpoint.Value) (closedOrders [ case types.OrderTypeStopLimit: // should we trigger the order? - if priceF <= o.StopPrice { + if price.Compare(o.StopPrice) <= 0 { askOrders = append(askOrders, o) break } @@ -304,7 +303,7 @@ func (m *SimplePriceMatching) BuyToPrice(price fixedpoint.Value) (closedOrders [ o.Type = types.OrderTypeLimit // is it a taker order? - if priceF >= o.Price { + if price.Compare(o.Price) >= 0 { o.ExecutedQuantity = o.Quantity o.Status = types.OrderStatusFilled closedOrders = append(closedOrders, o) @@ -321,7 +320,7 @@ func (m *SimplePriceMatching) BuyToPrice(price fixedpoint.Value) (closedOrders [ } case types.OrderTypeLimit, types.OrderTypeLimitMaker: - if priceF >= o.Price { + if price.Compare(o.Price) >= 0 { o.ExecutedQuantity = o.Quantity o.Status = types.OrderStatusFilled closedOrders = append(closedOrders, o) @@ -349,14 +348,14 @@ func (m *SimplePriceMatching) BuyToPrice(price fixedpoint.Value) (closedOrders [ } func (m *SimplePriceMatching) SellToPrice(price fixedpoint.Value) (closedOrders []types.Order, trades []types.Trade) { - var sellPrice = price.Float64() + var sellPrice = price var bidOrders []types.Order for _, o := range m.bidOrders { switch o.Type { case types.OrderTypeStopMarket: // should we trigger the order - if sellPrice <= o.StopPrice { + if sellPrice.Compare(o.StopPrice) <= 0 { o.ExecutedQuantity = o.Quantity o.Price = sellPrice o.Status = types.OrderStatusFilled @@ -374,10 +373,10 @@ func (m *SimplePriceMatching) SellToPrice(price fixedpoint.Value) (closedOrders case types.OrderTypeStopLimit: // should we trigger the order - if sellPrice <= o.StopPrice { + if sellPrice.Compare(o.StopPrice) <= 0 { o.Type = types.OrderTypeLimit - if sellPrice <= o.Price { + if sellPrice.Compare(o.Price) <= 0 { o.ExecutedQuantity = o.Quantity o.Status = types.OrderStatusFilled closedOrders = append(closedOrders, o) @@ -396,7 +395,7 @@ func (m *SimplePriceMatching) SellToPrice(price fixedpoint.Value) (closedOrders } case types.OrderTypeLimit, types.OrderTypeLimitMaker: - if sellPrice <= o.Price { + if sellPrice.Compare(o.Price) <= 0 { o.ExecutedQuantity = o.Quantity o.Status = types.OrderStatusFilled closedOrders = append(closedOrders, o) @@ -428,31 +427,31 @@ func (m *SimplePriceMatching) processKLine(kline types.KLine) { switch kline.Direction() { case types.DirectionDown: - if kline.High >= kline.Open { - m.BuyToPrice(fixedpoint.NewFromFloat(kline.High)) + if kline.High.Compare(kline.Open) >= 0 { + m.BuyToPrice(kline.High) } - if kline.Low > kline.Close { - m.SellToPrice(fixedpoint.NewFromFloat(kline.Low)) - m.BuyToPrice(fixedpoint.NewFromFloat(kline.Close)) + if kline.Low.Compare(kline.Close) > 0 { + m.SellToPrice(kline.Low) + m.BuyToPrice(kline.Close) } else { - m.SellToPrice(fixedpoint.NewFromFloat(kline.Close)) + m.SellToPrice(kline.Close) } case types.DirectionUp: - if kline.Low <= kline.Open { - m.SellToPrice(fixedpoint.NewFromFloat(kline.Low)) + if kline.Low.Compare(kline.Open) <= 0 { + m.SellToPrice(kline.Low) } - if kline.High > kline.Close { - m.BuyToPrice(fixedpoint.NewFromFloat(kline.High)) - m.SellToPrice(fixedpoint.NewFromFloat(kline.Close)) + if kline.High.Compare(kline.Close) > 0 { + m.BuyToPrice(kline.High) + m.SellToPrice(kline.Close) } else { - m.BuyToPrice(fixedpoint.NewFromFloat(kline.Close)) + m.BuyToPrice(kline.Close) } default: // no trade up or down - if m.LastPrice == 0 { - m.BuyToPrice(fixedpoint.NewFromFloat(kline.Close)) + if m.LastPrice.IsZero() { + m.BuyToPrice(kline.Close) } } @@ -464,7 +463,7 @@ func (m *SimplePriceMatching) newOrder(o types.SubmitOrder, orderID uint64) type SubmitOrder: o, Exchange: types.ExchangeBacktest, Status: types.OrderStatusNew, - ExecutedQuantity: 0, + ExecutedQuantity: fixedpoint.Zero, IsWorking: true, CreationTime: types.Time(m.CurrentTime), UpdateTime: types.Time(m.CurrentTime), diff --git a/pkg/backtest/priceorder.go b/pkg/backtest/priceorder.go index 9a0e52ae4..c74e636d4 100644 --- a/pkg/backtest/priceorder.go +++ b/pkg/backtest/priceorder.go @@ -15,7 +15,7 @@ type PriceOrder struct { type PriceOrderSlice []PriceOrder func (slice PriceOrderSlice) Len() int { return len(slice) } -func (slice PriceOrderSlice) Less(i, j int) bool { return slice[i].Price < slice[j].Price } +func (slice PriceOrderSlice) Less(i, j int) bool { return slice[i].Price.Compare(slice[j].Price) < 0 } func (slice PriceOrderSlice) Swap(i, j int) { slice[i], slice[j] = slice[j], slice[i] } func (slice PriceOrderSlice) InsertAt(idx int, po PriceOrder) PriceOrderSlice { @@ -47,9 +47,9 @@ func (slice PriceOrderSlice) First() (PriceOrder, bool) { func (slice PriceOrderSlice) Find(price fixedpoint.Value, descending bool) (pv PriceOrder, idx int) { idx = sort.Search(len(slice), func(i int) bool { if descending { - return slice[i].Price <= price + return slice[i].Price.Compare(price) <= 0 } - return slice[i].Price >= price + return slice[i].Price.Compare(price) >= 0 }) if idx >= len(slice) || slice[idx].Price != price { diff --git a/pkg/bbgo/environment.go b/pkg/bbgo/environment.go index 20c0b5f98..658d848d1 100644 --- a/pkg/bbgo/environment.go +++ b/pkg/bbgo/environment.go @@ -463,7 +463,7 @@ func (environ *Environment) BindSync(userConfig *Config) { orderWriter := func(order types.Order) { switch order.Status { case types.OrderStatusFilled, types.OrderStatusCanceled: - if order.ExecutedQuantity > 0.0 { + if order.ExecutedQuantity.Sign() > 0 { if err := environ.OrderService.Insert(order); err != nil { log.WithError(err).Errorf("order insert error: %+v", order) } diff --git a/pkg/bbgo/interact.go b/pkg/bbgo/interact.go index 2eb9b190d..cb13a13db 100644 --- a/pkg/bbgo/interact.go +++ b/pkg/bbgo/interact.go @@ -10,10 +10,11 @@ import ( "github.com/c9s/bbgo/pkg/interact" "github.com/c9s/bbgo/pkg/types" + "github.com/c9s/bbgo/pkg/fixedpoint" ) type PositionCloser interface { - ClosePosition(ctx context.Context, percentage float64) error + ClosePosition(ctx context.Context, percentage fixedpoint.Value) error } type PositionReader interface { @@ -23,7 +24,7 @@ type PositionReader interface { type closePositionContext struct { signature string closer PositionCloser - percentage float64 + percentage fixedpoint.Value } type CoreInteraction struct { @@ -75,7 +76,7 @@ func (it *CoreInteraction) Commands(i *interact.Interact) { message := "Your balances\n" balances := session.Account.Balances() for _, balance := range balances { - if balance.Total() == 0 { + if balance.Total().IsZero() { continue } @@ -121,7 +122,7 @@ func (it *CoreInteraction) Commands(i *interact.Interact) { reply.Send("Your current position:") reply.Send(position.PlainText()) - if position.Base == 0 { + if position.Base.IsZero() { reply.Message(fmt.Sprintf("Strategy %q has no opened position", signature)) return fmt.Errorf("strategy %T has no opened position", strategy) } @@ -173,7 +174,7 @@ func (it *CoreInteraction) Commands(i *interact.Interact) { reply.Send("Your current position:") reply.Send(position.PlainText()) - if position.Base == 0 { + if position.Base.IsZero() { reply.Message("No opened position") if kc, ok := reply.(interact.KeyboardController) ; ok { kc.RemoveKeyboard() @@ -190,7 +191,7 @@ func (it *CoreInteraction) Commands(i *interact.Interact) { return nil }).Next(func(percentageStr string, reply interact.Reply) error { - percentage, err := parseFloatPercent(percentageStr, 64) + percentage, err := fixedpoint.NewFromString(percentageStr) if err != nil { reply.Message(fmt.Sprintf("%q is not a valid percentage string", percentageStr)) return err diff --git a/pkg/bbgo/order_execution.go b/pkg/bbgo/order_execution.go index 0328b6e7a..e9310af4c 100644 --- a/pkg/bbgo/order_execution.go +++ b/pkg/bbgo/order_execution.go @@ -3,7 +3,6 @@ package bbgo import ( "context" "fmt" - "math" "github.com/pkg/errors" log "github.com/sirupsen/logrus" @@ -122,8 +121,10 @@ func (c *BasicRiskController) ProcessOrders(session *ExchangeSession, orders ... errs = append(errs, err) } - accumulativeQuoteAmount := 0.0 - accumulativeBaseSellQuantity := 0.0 + accumulativeQuoteAmount := fixedpoint.Zero + accumulativeBaseSellQuantity := fixedpoint.Zero + increaseFactor := fixedpoint.NewFromFloat(1.01) + for _, order := range orders { lastPrice, ok := session.LastPrice(order.Symbol) if !ok { @@ -146,6 +147,7 @@ func (c *BasicRiskController) ProcessOrders(session *ExchangeSession, orders ... switch order.Side { case types.SideTypeBuy: + minAmount := market.MinAmount.Mul(increaseFactor) // Critical conditions for placing buy orders quoteBalance, ok := balances[market.QuoteCurrency] if !ok { @@ -153,67 +155,70 @@ func (c *BasicRiskController) ProcessOrders(session *ExchangeSession, orders ... continue } - if quoteBalance.Available < c.MinQuoteBalance { + if quoteBalance.Available.Compare(c.MinQuoteBalance) < 0 { addError(errors.Wrapf(ErrQuoteBalanceLevelTooLow, "can not place buy order, quote balance level is too low: %s < %s, order: %s", - types.USD.FormatMoneyFloat64(quoteBalance.Available.Float64()), - types.USD.FormatMoneyFloat64(c.MinQuoteBalance.Float64()), order.String())) + types.USD.FormatMoney(quoteBalance.Available), + types.USD.FormatMoney(c.MinQuoteBalance), order.String())) continue } // Increase the quantity if the amount is not enough, // this is the only increase op, later we will decrease the quantity if it meets the criteria - quantity = AdjustFloatQuantityByMinAmount(quantity, price, market.MinAmount*1.01) + quantity = AdjustFloatQuantityByMinAmount(quantity, price, minAmount) - if c.MaxOrderAmount > 0 { - quantity = AdjustFloatQuantityByMaxAmount(quantity, price, c.MaxOrderAmount.Float64()) + if c.MaxOrderAmount.Sign() > 0 { + quantity = AdjustFloatQuantityByMaxAmount(quantity, price, c.MaxOrderAmount) } - quoteAssetQuota := math.Max(0.0, quoteBalance.Available.Float64()-c.MinQuoteBalance.Float64()) - if quoteAssetQuota < market.MinAmount { + quoteAssetQuota := fixedpoint.Max( + fixedpoint.Zero, quoteBalance.Available.Sub(c.MinQuoteBalance)) + if quoteAssetQuota.Compare(market.MinAmount) < 0 { addError( errors.Wrapf( ErrInsufficientQuoteBalance, - "can not place buy order, insufficient quote balance: quota %f < min amount %f, order: %s", - quoteAssetQuota, market.MinAmount, order.String())) + "can not place buy order, insufficient quote balance: quota %s < min amount %s, order: %s", + quoteAssetQuota.String(), market.MinAmount.String(), order.String())) continue } quantity = AdjustFloatQuantityByMaxAmount(quantity, price, quoteAssetQuota) // if MaxBaseAssetBalance is enabled, we should check the current base asset balance - if baseBalance, hasBaseAsset := balances[market.BaseCurrency]; hasBaseAsset && c.MaxBaseAssetBalance > 0 { - if baseBalance.Available > c.MaxBaseAssetBalance { + if baseBalance, hasBaseAsset := balances[market.BaseCurrency]; hasBaseAsset && c.MaxBaseAssetBalance.Sign() > 0 { + if baseBalance.Available.Compare(c.MaxBaseAssetBalance) > 0 { addError( errors.Wrapf( ErrAssetBalanceLevelTooHigh, - "should not place buy order, asset balance level is too high: %f > %f, order: %s", - baseBalance.Available.Float64(), - c.MaxBaseAssetBalance.Float64(), + "should not place buy order, asset balance level is too high: %s > %s, order: %s", + baseBalance.Available.String(), + c.MaxBaseAssetBalance.String(), order.String())) continue } - baseAssetQuota := math.Max(0.0, c.MaxBaseAssetBalance.Float64()-baseBalance.Available.Float64()) - if quantity > baseAssetQuota { + baseAssetQuota := fixedpoint.Max(fixedpoint.Zero, c.MaxBaseAssetBalance.Sub(baseBalance.Available)) + if quantity.Compare(baseAssetQuota) > 0 { quantity = baseAssetQuota } } // if the amount is still too small, we should skip it. - notional := quantity * lastPrice - if notional < market.MinAmount { + notional := quantity.Mul(lastPrice) + if notional.Compare(market.MinAmount) < 0 { addError( fmt.Errorf( - "can not place buy order, quote amount too small: notional %f < min amount %f, order: %s", - notional, - market.MinAmount, + "can not place buy order, quote amount too small: notional %s < min amount %s, order: %s", + notional.String(), + market.MinAmount.String(), order.String())) continue } - accumulativeQuoteAmount += notional + accumulativeQuoteAmount = accumulativeQuoteAmount.Add(notional) case types.SideTypeSell: + minNotion := market.MinNotional.Mul(increaseFactor) + // Critical conditions for placing SELL orders baseAssetBalance, ok := balances[market.BaseCurrency] if !ok { @@ -226,58 +231,58 @@ func (c *BasicRiskController) ProcessOrders(session *ExchangeSession, orders ... } // if the amount is too small, we should increase it. - quantity = AdjustFloatQuantityByMinAmount(quantity, price, market.MinNotional*1.01) + quantity = AdjustFloatQuantityByMinAmount(quantity, price, minNotion) // we should not SELL too much - quantity = math.Min(quantity, baseAssetBalance.Available.Float64()) + quantity = fixedpoint.Min(quantity, baseAssetBalance.Available) - if c.MinBaseAssetBalance > 0 { - if baseAssetBalance.Available < c.MinBaseAssetBalance { + if c.MinBaseAssetBalance.Sign() > 0 { + if baseAssetBalance.Available.Compare(c.MinBaseAssetBalance) < 0 { addError( errors.Wrapf( ErrAssetBalanceLevelTooLow, - "asset balance level is too low: %f > %f", baseAssetBalance.Available.Float64(), c.MinBaseAssetBalance.Float64())) + "asset balance level is too low: %s > %s", baseAssetBalance.Available.String(), c.MinBaseAssetBalance.String())) continue } - quantity = math.Min(quantity, baseAssetBalance.Available.Float64()-c.MinBaseAssetBalance.Float64()) - if quantity < market.MinQuantity { + quantity = fixedpoint.Min(quantity, baseAssetBalance.Available.Sub(c.MinBaseAssetBalance)) + if quantity.Compare(market.MinQuantity) < 0 { addError( errors.Wrapf( ErrInsufficientAssetBalance, - "insufficient asset balance: %f > minimal quantity %f", - baseAssetBalance.Available.Float64(), - market.MinQuantity)) + "insufficient asset balance: %s > minimal quantity %s", + baseAssetBalance.Available.String(), + market.MinQuantity.String())) continue } } - if c.MaxOrderAmount > 0 { - quantity = AdjustFloatQuantityByMaxAmount(quantity, price, c.MaxOrderAmount.Float64()) + if c.MaxOrderAmount.Sign() > 0 { + quantity = AdjustFloatQuantityByMaxAmount(quantity, price, c.MaxOrderAmount) } - notional := quantity * lastPrice - if notional < market.MinNotional { + notional := quantity.Mul(lastPrice) + if notional.Compare(market.MinNotional) < 0 { addError( fmt.Errorf( - "can not place sell order, notional %f < min notional: %f, order: %s", - notional, - market.MinNotional, + "can not place sell order, notional %s < min notional: %s, order: %s", + notional.String(), + market.MinNotional.String(), order.String())) continue } - if quantity < market.MinQuantity { + if quantity.Compare(market.MinQuantity) < 0 { addError( fmt.Errorf( - "can not place sell order, quantity %f is less than the minimal lot %f, order: %s", - quantity, - market.MinQuantity, + "can not place sell order, quantity %s is less than the minimal lot %s, order: %s", + quantity.String(), + market.MinQuantity.String(), order.String())) continue } - accumulativeBaseSellQuantity += quantity + accumulativeBaseSellQuantity = accumulativeBaseSellQuantity.Add(quantity) } // update quantity and format the order diff --git a/pkg/bbgo/order_processor.go b/pkg/bbgo/order_processor.go index 661975fdb..f61f13461 100644 --- a/pkg/bbgo/order_processor.go +++ b/pkg/bbgo/order_processor.go @@ -18,7 +18,7 @@ var ( func AdjustQuantityByMaxAmount(quantity, currentPrice, maxAmount fixedpoint.Value) fixedpoint.Value { // modify quantity for the min amount amount := currentPrice.Mul(quantity) - if amount < maxAmount { + if amount.Compare(maxAmount) < 0 { return quantity } @@ -30,7 +30,7 @@ func AdjustQuantityByMaxAmount(quantity, currentPrice, maxAmount fixedpoint.Valu func AdjustQuantityByMinAmount(quantity, currentPrice, minAmount fixedpoint.Value) fixedpoint.Value { // modify quantity for the min amount amount := currentPrice.Mul(quantity) - if amount < minAmount { + if amount.Compare(minAmount) < 0 { ratio := minAmount.Div(amount) quantity = quantity.Mul(ratio) } @@ -39,22 +39,22 @@ func AdjustQuantityByMinAmount(quantity, currentPrice, minAmount fixedpoint.Valu } // AdjustFloatQuantityByMinAmount adjusts the quantity to make the amount greater than the given minAmount -func AdjustFloatQuantityByMinAmount(quantity, currentPrice, minAmount float64) float64 { +func AdjustFloatQuantityByMinAmount(quantity, currentPrice, minAmount fixedpoint.Value) fixedpoint.Value { // modify quantity for the min amount - amount := currentPrice * quantity - if amount < minAmount { - ratio := minAmount / amount - quantity *= ratio + amount := currentPrice.Mul(quantity) + if amount.Compare(minAmount) < 0 { + ratio := minAmount.Div(amount) + return quantity.Mul(ratio) } return quantity } -func AdjustFloatQuantityByMaxAmount(quantity float64, price float64, maxAmount float64) float64 { - amount := price * quantity - if amount > maxAmount { - ratio := maxAmount / amount - quantity *= ratio +func AdjustFloatQuantityByMaxAmount(quantity fixedpoint.Value, price fixedpoint.Value, maxAmount fixedpoint.Value) fixedpoint.Value { + amount := price.Mul(quantity) + if amount.Compare(maxAmount) > 0 { + ratio := maxAmount.Div(amount) + return quantity.Mul(ratio) } return quantity diff --git a/pkg/bbgo/order_store.go b/pkg/bbgo/order_store.go index 1564e564c..46e4911c7 100644 --- a/pkg/bbgo/order_store.go +++ b/pkg/bbgo/order_store.go @@ -140,7 +140,7 @@ func (s *OrderStore) handleOrderUpdate(order types.Order) { case types.OrderStatusCanceled: if s.RemoveCancelled { s.Remove(order) - } else if order.ExecutedQuantity == 0.0 { + } else if order.ExecutedQuantity.IsZero() { s.Remove(order) } diff --git a/pkg/bbgo/profitstats.go b/pkg/bbgo/profitstats.go index c77cc1ec4..0b46307a1 100644 --- a/pkg/bbgo/profitstats.go +++ b/pkg/bbgo/profitstats.go @@ -48,7 +48,7 @@ func (p *Profit) SlackAttachment() slack.Attachment { var fields []slack.AttachmentField - if p.NetProfit != 0 { + if !p.NetProfit.IsZero() { fields = append(fields, slack.AttachmentField{ Title: "Net Profit", Value: pnlSignString(p.NetProfit) + " " + p.QuoteCurrency, @@ -56,7 +56,7 @@ func (p *Profit) SlackAttachment() slack.Attachment { }) } - if p.ProfitMargin != 0 { + if !p.ProfitMargin.IsZero() { fields = append(fields, slack.AttachmentField{ Title: "Profit Margin", Value: p.ProfitMargin.Percentage(), @@ -64,7 +64,7 @@ func (p *Profit) SlackAttachment() slack.Attachment { }) } - if p.NetProfitMargin != 0 { + if !p.NetProfitMargin.IsZero() { fields = append(fields, slack.AttachmentField{ Title: "Net Profit Margin", Value: p.NetProfitMargin.Percentage(), @@ -72,7 +72,7 @@ func (p *Profit) SlackAttachment() slack.Attachment { }) } - if p.TradeAmount != 0.0 { + if !p.TradeAmount.IsZero() { fields = append(fields, slack.AttachmentField{ Title: "Trade Amount", Value: p.TradeAmount.String() + " " + p.QuoteCurrency, @@ -80,7 +80,7 @@ func (p *Profit) SlackAttachment() slack.Attachment { }) } - if p.FeeInUSD != 0 { + if !p.FeeInUSD.IsZero() { fields = append(fields, slack.AttachmentField{ Title: "Fee In USD", Value: p.FeeInUSD.String() + " USD", @@ -106,19 +106,19 @@ func (p *Profit) SlackAttachment() slack.Attachment { func (p *Profit) PlainText() string { var emoji string - if p.ProfitMargin != 0 { + if !p.ProfitMargin.IsZero() { emoji = pnlEmojiMargin(p.Profit, p.ProfitMargin, defaultPnlLevelResolution) } else { emoji = pnlEmojiSimple(p.Profit) } - return fmt.Sprintf("%s trade profit %s %f %s (%.2f%%), net profit =~ %f %s (%.2f%%)", + return fmt.Sprintf("%s trade profit %s %s %s (%s), net profit =~ %s %s (%s)", p.Symbol, emoji, - p.Profit.Float64(), p.QuoteCurrency, - p.ProfitMargin.Float64()*100.0, - p.NetProfit.Float64(), p.QuoteCurrency, - p.NetProfitMargin.Float64()*100.0, + p.Profit.String(), p.QuoteCurrency, + p.ProfitMargin.Percentage(), + p.NetProfit.String(), p.QuoteCurrency, + p.NetProfitMargin.Percentage(), ) } @@ -127,25 +127,25 @@ var profitEmoji = "💰" var defaultPnlLevelResolution = fixedpoint.NewFromFloat(0.001) func pnlColor(pnl fixedpoint.Value) string { - if pnl > 0 { + if pnl.Sign() > 0 { return types.GreenColor } return types.RedColor } func pnlSignString(pnl fixedpoint.Value) string { - if pnl > 0 { + if pnl.Sign() > 0 { return "+" + pnl.String() } return pnl.String() } func pnlEmojiSimple(pnl fixedpoint.Value) string { - if pnl < 0 { + if pnl.Sign() < 0 { return lossEmoji } - if pnl == 0 { + if pnl.IsZero() { return "" } @@ -153,26 +153,26 @@ func pnlEmojiSimple(pnl fixedpoint.Value) string { } func pnlEmojiMargin(pnl, margin, resolution fixedpoint.Value) (out string) { - if margin == 0 { + if margin.IsZero() { return pnlEmojiSimple(pnl) } - if pnl < 0 { + if pnl.Sign() < 0 { out = lossEmoji - level := (-margin).Div(resolution).Floor() - for i := 1; i < level.Int(); i++ { + level := (margin.Neg()).Div(resolution).Int() + for i := 1; i < level; i++ { out += lossEmoji } return out } - if pnl == 0 { + if pnl.IsZero() { return out } out = profitEmoji - level := margin.Div(resolution).Floor() - for i := 1; i < level.Int(); i++ { + level := margin.Div(resolution).Int() + for i := 1; i < level; i++ { out += profitEmoji } return out @@ -207,17 +207,17 @@ func (s *ProfitStats) Init(market types.Market) { } func (s *ProfitStats) AddProfit(profit Profit) { - s.AccumulatedPnL += profit.Profit - s.AccumulatedNetProfit += profit.NetProfit - s.TodayPnL += profit.Profit - s.TodayNetProfit += profit.NetProfit + s.AccumulatedPnL = s.AccumulatedPnL.Add(profit.Profit) + s.AccumulatedNetProfit = s.AccumulatedNetProfit.Add(profit.NetProfit) + s.TodayPnL = s.TodayPnL.Add(profit.Profit) + s.TodayNetProfit = s.TodayNetProfit.Add(profit.NetProfit) - if profit.Profit < 0 { - s.AccumulatedLoss += profit.Profit - s.TodayLoss += profit.Profit - } else if profit.Profit > 0 { - s.AccumulatedProfit += profit.Profit - s.TodayProfit += profit.Profit + if profit.Profit.Sign() < 0 { + s.AccumulatedLoss = s.AccumulatedLoss.Add(profit.Profit) + s.TodayLoss = s.TodayLoss.Add(profit.Profit) + } else if profit.Profit.Sign() > 0 { + s.AccumulatedProfit = s.AccumulatedLoss.Add(profit.Profit) + s.TodayProfit = s.TodayProfit.Add(profit.Profit) } } @@ -226,7 +226,7 @@ func (s *ProfitStats) AddTrade(trade types.Trade) { s.ResetToday() } - s.AccumulatedVolume += fixedpoint.NewFromFloat(trade.Quantity) + s.AccumulatedVolume = s.AccumulatedVolume.Add(trade.Quantity) } func (s *ProfitStats) IsOver24Hours() bool { @@ -234,10 +234,10 @@ func (s *ProfitStats) IsOver24Hours() bool { } func (s *ProfitStats) ResetToday() { - s.TodayPnL = 0 - s.TodayNetProfit = 0 - s.TodayProfit = 0 - s.TodayLoss = 0 + s.TodayPnL = fixedpoint.Zero + s.TodayNetProfit = fixedpoint.Zero + s.TodayProfit = fixedpoint.Zero + s.TodayLoss = fixedpoint.Zero var beginningOfTheDay = util.BeginningOfTheDay(time.Now().Local()) s.TodaySince = beginningOfTheDay.Unix() @@ -246,21 +246,21 @@ func (s *ProfitStats) ResetToday() { func (s *ProfitStats) PlainText() string { since := time.Unix(s.AccumulatedSince, 0).Local() return fmt.Sprintf("%s Profit Today\n"+ - "Profit %f %s\n"+ - "Net profit %f %s\n"+ - "Trade Loss %f %s\n"+ + "Profit %s %s\n"+ + "Net profit %s %s\n"+ + "Trade Loss %s %s\n"+ "Summary:\n"+ - "Accumulated Profit %f %s\n"+ - "Accumulated Net Profit %f %s\n"+ - "Accumulated Trade Loss %f %s\n"+ + "Accumulated Profit %s %s\n"+ + "Accumulated Net Profit %s %s\n"+ + "Accumulated Trade Loss %s %s\n"+ "Since %s", s.Symbol, - s.TodayPnL.Float64(), s.QuoteCurrency, - s.TodayNetProfit.Float64(), s.QuoteCurrency, - s.TodayLoss.Float64(), s.QuoteCurrency, - s.AccumulatedPnL.Float64(), s.QuoteCurrency, - s.AccumulatedNetProfit.Float64(), s.QuoteCurrency, - s.AccumulatedLoss.Float64(), s.QuoteCurrency, + s.TodayPnL.String(), s.QuoteCurrency, + s.TodayNetProfit.String(), s.QuoteCurrency, + s.TodayLoss.String(), s.QuoteCurrency, + s.AccumulatedPnL.String(), s.QuoteCurrency, + s.AccumulatedNetProfit.String(), s.QuoteCurrency, + s.AccumulatedLoss.String(), s.QuoteCurrency, since.Format(time.RFC822), ) } @@ -274,7 +274,7 @@ func (s *ProfitStats) SlackAttachment() slack.Attachment { var fields []slack.AttachmentField - if s.TodayPnL != 0 { + if !s.TodayPnL.IsZero() { fields = append(fields, slack.AttachmentField{ Title: "P&L Today", Value: pnlSignString(s.TodayPnL) + " " + s.QuoteCurrency, @@ -282,7 +282,7 @@ func (s *ProfitStats) SlackAttachment() slack.Attachment { }) } - if s.TodayProfit != 0 { + if !s.TodayProfit.IsZero() { fields = append(fields, slack.AttachmentField{ Title: "Profit Today", Value: pnlSignString(s.TodayProfit) + " " + s.QuoteCurrency, @@ -290,7 +290,7 @@ func (s *ProfitStats) SlackAttachment() slack.Attachment { }) } - if s.TodayNetProfit != 0 { + if !s.TodayNetProfit.IsZero() { fields = append(fields, slack.AttachmentField{ Title: "Net Profit Today", Value: pnlSignString(s.TodayNetProfit) + " " + s.QuoteCurrency, @@ -298,7 +298,7 @@ func (s *ProfitStats) SlackAttachment() slack.Attachment { }) } - if s.TodayLoss != 0 { + if !s.TodayLoss.IsZero() { fields = append(fields, slack.AttachmentField{ Title: "Loss Today", Value: pnlSignString(s.TodayLoss) + " " + s.QuoteCurrency, @@ -306,28 +306,28 @@ func (s *ProfitStats) SlackAttachment() slack.Attachment { }) } - if s.AccumulatedPnL != 0 { + if !s.AccumulatedPnL.IsZero() { fields = append(fields, slack.AttachmentField{ Title: "Accumulated P&L", Value: pnlSignString(s.AccumulatedPnL) + " " + s.QuoteCurrency, }) } - if s.AccumulatedProfit != 0 { + if !s.AccumulatedProfit.IsZero() { fields = append(fields, slack.AttachmentField{ Title: "Accumulated Profit", Value: pnlSignString(s.AccumulatedProfit) + " " + s.QuoteCurrency, }) } - if s.AccumulatedNetProfit != 0 { + if !s.AccumulatedNetProfit.IsZero() { fields = append(fields, slack.AttachmentField{ Title: "Accumulated Net Profit", Value: pnlSignString(s.AccumulatedNetProfit) + " " + s.QuoteCurrency, }) } - if s.AccumulatedLoss != 0 { + if !s.AccumulatedLoss.IsZero() { fields = append(fields, slack.AttachmentField{ Title: "Accumulated Loss", Value: pnlSignString(s.AccumulatedLoss) + " " + s.QuoteCurrency, diff --git a/pkg/bbgo/quantity_amount.go b/pkg/bbgo/quantity_amount.go index 4940684b0..58b238aca 100644 --- a/pkg/bbgo/quantity_amount.go +++ b/pkg/bbgo/quantity_amount.go @@ -18,11 +18,11 @@ type QuantityOrAmount struct { } func (qa *QuantityOrAmount) IsSet() bool { - return qa.Quantity > 0 || qa.Amount > 0 + return qa.Quantity.Sign() > 0 || qa.Amount.Sign() > 0 } func (qa *QuantityOrAmount) Validate() error { - if qa.Quantity == 0 && qa.Amount == 0 { + if qa.Quantity.IsZero() && qa.Amount.IsZero() { return errors.New("either quantity or amount can not be empty") } return nil @@ -31,7 +31,7 @@ func (qa *QuantityOrAmount) Validate() error { // CalculateQuantity calculates the equivalent quantity of the given price when amount is set // it returns the quantity if the quantity is set func (qa *QuantityOrAmount) CalculateQuantity(currentPrice fixedpoint.Value) fixedpoint.Value { - if qa.Amount > 0 { + if qa.Amount.Sign() > 0 { quantity := qa.Amount.Div(currentPrice) return quantity } diff --git a/pkg/bbgo/quota.go b/pkg/bbgo/quota.go index 377774da4..e4773d9af 100644 --- a/pkg/bbgo/quota.go +++ b/pkg/bbgo/quota.go @@ -14,18 +14,18 @@ type Quota struct { func (q *Quota) Add(fund fixedpoint.Value) { q.mu.Lock() - q.Available += fund + q.Available = q.Available.Add(fund) q.mu.Unlock() } func (q *Quota) Lock(fund fixedpoint.Value) bool { - if fund > q.Available { + if fund.Compare(q.Available) > 0 { return false } q.mu.Lock() - q.Available -= fund - q.Locked += fund + q.Available = q.Available.Sub(fund) + q.Locked = q.Locked.Add(fund) q.mu.Unlock() return true @@ -33,14 +33,14 @@ func (q *Quota) Lock(fund fixedpoint.Value) bool { func (q *Quota) Commit() { q.mu.Lock() - q.Locked = 0 + q.Locked = fixedpoint.Zero q.mu.Unlock() } func (q *Quota) Rollback() { q.mu.Lock() - q.Available += q.Locked - q.Locked = 0 + q.Available = q.Available.Add(q.Locked) + q.Locked = fixedpoint.Zero q.mu.Unlock() } diff --git a/pkg/bbgo/session.go b/pkg/bbgo/session.go index 0c257997c..4aa187853 100644 --- a/pkg/bbgo/session.go +++ b/pkg/bbgo/session.go @@ -93,10 +93,10 @@ func NewStandardIndicatorSet(symbol string, store *MarketDataStore) *StandardInd // BOLL returns the bollinger band indicator of the given interval and the window, // Please note that the K for std dev is fixed and defaults to 2.0 -func (set *StandardIndicatorSet) BOLL(iw types.IntervalWindow, bandWidth float64) *indicator.BOLL { +func (set *StandardIndicatorSet) BOLL(iw types.IntervalWindow, bandWidth fixedpoint.Value) *indicator.BOLL { inc, ok := set.boll[iw] if !ok { - inc = &indicator.BOLL{IntervalWindow: iw, K: bandWidth} + inc = &indicator.BOLL{IntervalWindow: iw, K: bandWidth.Float64()} inc.Bind(set.store) set.boll[iw] = inc } @@ -545,17 +545,17 @@ func (session *ExchangeSession) OrderBook(symbol string) (s *types.StreamOrderBo return s, ok } -func (session *ExchangeSession) StartPrice(symbol string) (price float64, ok bool) { +func (session *ExchangeSession) StartPrice(symbol string) (price fixedpoint.Value, ok bool) { price, ok = session.startPrices[symbol] return price, ok } -func (session *ExchangeSession) LastPrice(symbol string) (price float64, ok bool) { +func (session *ExchangeSession) LastPrice(symbol string) (price fixedpoint.Value, ok bool) { price, ok = session.lastPrices[symbol] return price, ok } -func (session *ExchangeSession) LastPrices() map[string]float64 { +func (session *ExchangeSession) LastPrices() map[string]fixedpoint.Value { return session.lastPrices } @@ -644,7 +644,7 @@ func (session *ExchangeSession) FindPossibleSymbols() (symbols []string, err err var fiatAssets []string for _, currency := range types.FiatCurrencies { - if balance, ok := balances[currency]; ok && balance.Total() > 0 { + if balance, ok := balances[currency]; ok && balance.Total().Sign() > 0 { fiatAssets = append(fiatAssets, currency) } } @@ -659,7 +659,7 @@ func (session *ExchangeSession) FindPossibleSymbols() (symbols []string, err err // ignore the asset that we don't have in the balance sheet balance, hasAsset := balances[market.BaseCurrency] - if !hasAsset || balance.Total() == 0 { + if !hasAsset || balance.Total().IsZero() { continue } @@ -740,8 +740,8 @@ func (session *ExchangeSession) InitExchange(name string, exchange types.Exchang session.orderBooks = make(map[string]*types.StreamOrderBook) session.markets = make(map[string]types.Market) - session.lastPrices = make(map[string]float64) - session.startPrices = make(map[string]float64) + session.lastPrices = make(map[string]fixedpoint.Value) + session.startPrices = make(map[string]fixedpoint.Value) session.marketDataStores = make(map[string]*MarketDataStore) session.positions = make(map[string]*types.Position) session.standardIndicatorSets = make(map[string]*StandardIndicatorSet) @@ -812,7 +812,7 @@ func (session *ExchangeSession) metricsTradeUpdater(trade types.Trade) { "symbol": trade.Symbol, "liquidity": trade.Liquidity(), } - metricsTradingVolume.With(labels).Add(trade.Quantity * trade.Price) + metricsTradingVolume.With(labels).Add(trade.Quantity.Mul(trade.Price).Float64()) metricsTradesTotal.With(labels).Inc() metricsLastUpdateTimeBalance.With(prometheus.Labels{ "exchange": session.ExchangeName.String(), diff --git a/pkg/bbgo/smart_stops.go b/pkg/bbgo/smart_stops.go index 2279e5539..7deb4f500 100644 --- a/pkg/bbgo/smart_stops.go +++ b/pkg/bbgo/smart_stops.go @@ -3,7 +3,6 @@ package bbgo import ( "context" "errors" - "math" log "github.com/sirupsen/logrus" @@ -37,7 +36,7 @@ type TrailingStopController struct { Symbol string position *types.Position - latestHigh float64 + latestHigh fixedpoint.Value averageCost fixedpoint.Value // activated: when the price reaches the min profit price, we set the activated to true to enable trailing stop @@ -74,25 +73,25 @@ func (c *TrailingStopController) Run(ctx context.Context, session *ExchangeSessi } // if average cost is zero, we don't need trailing stop - if c.averageCost == 0 || c.position == nil { + if c.averageCost.IsZero() || c.position == nil { return } closePrice := kline.Close // if we don't hold position, we just skip dust position - if c.position.Base.Abs().Float64() < c.position.Market.MinQuantity || c.position.Base.Abs().Float64()*closePrice < c.position.Market.MinNotional { + if c.position.Base.Abs().Compare(c.position.Market.MinQuantity) < 0 || c.position.Base.Abs().Mul(closePrice).Compare(c.position.Market.MinNotional) < 0 { return } - if c.MinProfit <= 0 { + if c.MinProfit.Sign() <= 0 { // when minProfit is not set, we should always activate the trailing stop order c.activated = true - } else if closePrice > c.averageCost.Float64() || - changeRate(closePrice, c.averageCost.Float64()) > c.MinProfit.Float64() { + } else if closePrice.Compare(c.averageCost) > 0 || + changeRate(closePrice, c.averageCost).Compare(c.MinProfit) > 0 { if !c.activated { - log.Infof("%s trailing stop activated at price %f", c.Symbol, closePrice) + log.Infof("%s trailing stop activated at price %s", c.Symbol, closePrice.String()) c.activated = true } } else { @@ -105,37 +104,37 @@ func (c *TrailingStopController) Run(ctx context.Context, session *ExchangeSessi // if the trailing stop order is activated, we should update the latest high // update the latest high - c.latestHigh = math.Max(closePrice, c.latestHigh) + c.latestHigh = fixedpoint.Max(closePrice, c.latestHigh) // if it's in the callback rate, we don't want to trigger stop - if closePrice < c.latestHigh && changeRate(closePrice, c.latestHigh) < c.CallbackRate.Float64() { + if closePrice.Compare(c.latestHigh) < 0 && changeRate(closePrice, c.latestHigh).Compare(c.CallbackRate) < 0 { return } if c.Virtual { // if the profit rate is defined, and it is less than our minimum profit rate, we skip stop - if c.MinProfit > 0 && - (closePrice < c.averageCost.Float64() || - changeRate(closePrice, c.averageCost.Float64()) < c.MinProfit.Float64()) { + if c.MinProfit.Sign() > 0 && + closePrice.Compare(c.averageCost) < 0 || + changeRate(closePrice, c.averageCost).Compare(c.MinProfit) < 0 { return } - log.Infof("%s trailing stop emitted, latest high: %f, closed price: %f, average cost: %f, profit spread: %f", + log.Infof("%s trailing stop emitted, latest high: %s, closed price: %s, average cost: %s, profit spread: %s", c.Symbol, - c.latestHigh, - closePrice, - c.averageCost.Float64(), - closePrice-c.averageCost.Float64()) + c.latestHigh.String(), + closePrice.String(), + c.averageCost.String(), + closePrice.Sub(c.averageCost).String()) log.Infof("current %s position: %s", c.Symbol, c.position.String()) - marketOrder := c.position.NewClosePositionOrder(c.ClosePosition.Float64()) + marketOrder := c.position.NewClosePositionOrder(c.ClosePosition) if marketOrder != nil { log.Infof("submitting %s market order to stop: %+v", c.Symbol, marketOrder) // skip dust order - if marketOrder.Quantity*closePrice < c.position.Market.MinNotional { - log.Warnf("%s market order quote quantity %f < min notional %f, skip placing order", c.Symbol, marketOrder.Quantity*closePrice, c.position.Market.MinNotional) + if marketOrder.Quantity.Mul(closePrice).Compare(c.position.Market.MinNotional) < 0 { + log.Warnf("%s market order quote quantity %s < min notional %s, skip placing order", c.Symbol, marketOrder.Quantity.Mul(closePrice).String(), c.position.Market.MinNotional.String()) return } @@ -148,16 +147,16 @@ func (c *TrailingStopController) Run(ctx context.Context, session *ExchangeSessi tradeCollector.Process() // reset the state - c.latestHigh = 0.0 + c.latestHigh = fixedpoint.Zero c.activated = false } } else { // place stop order only when the closed price is greater than the current average cost - if c.MinProfit > 0 && closePrice > c.averageCost.Float64() && - changeRate(closePrice, c.averageCost.Float64()) >= c.MinProfit.Float64() { + if c.MinProfit.Sign() > 0 && closePrice.Compare(c.averageCost) > 0 && + changeRate(closePrice, c.averageCost).Compare(c.MinProfit) >= 0 { - stopPrice := c.averageCost.MulFloat64(1.0 + c.MinProfit.Float64()) - orderForm := c.GenerateStopOrder(stopPrice.Float64(), c.averageCost.Float64()) + stopPrice := c.averageCost.Mul(fixedpoint.One.Add(c.MinProfit)) + orderForm := c.GenerateStopOrder(stopPrice, c.averageCost) if orderForm != nil { log.Infof("updating %s stop limit order to simulate trailing stop order...", c.Symbol) @@ -175,26 +174,27 @@ func (c *TrailingStopController) Run(ctx context.Context, session *ExchangeSessi }) } -func (c *TrailingStopController) GenerateStopOrder(stopPrice, price float64) *types.SubmitOrder { +func (c *TrailingStopController) GenerateStopOrder(stopPrice, price fixedpoint.Value) *types.SubmitOrder { base := c.position.GetBase() - if base == 0 { + if base.IsZero() { return nil } - quantity := math.Abs(base.Float64()) - quoteQuantity := price * quantity + quantity := base.Abs() + quoteQuantity := price.Mul(quantity) - if c.ClosePosition > 0 { - quantity = quantity * c.ClosePosition.Float64() + if c.ClosePosition.Sign() > 0 { + quantity = quantity.Mul(c.ClosePosition) } // skip dust orders - if quantity < c.position.Market.MinQuantity || quoteQuantity < c.position.Market.MinNotional { + if quantity.Compare(c.position.Market.MinQuantity) < 0 || + quoteQuantity.Compare(c.position.Market.MinNotional) < 0 { return nil } side := types.SideTypeSell - if base < 0 { + if base.Sign() < 0 { side = types.SideTypeBuy } @@ -282,6 +282,6 @@ func (s *SmartStops) RunStopControllers(ctx context.Context, session *ExchangeSe } } -func changeRate(a, b float64) float64 { - return math.Abs(a-b) / b +func changeRate(a, b fixedpoint.Value) fixedpoint.Value { + return a.Sub(b).Div(b).Abs() } diff --git a/pkg/bbgo/twap_order_executor.go b/pkg/bbgo/twap_order_executor.go index 1cb1b56f8..ea534bda8 100644 --- a/pkg/bbgo/twap_order_executor.go +++ b/pkg/bbgo/twap_order_executor.go @@ -93,56 +93,56 @@ func (e *TwapExecution) newBestPriceOrder() (orderForm types.SubmitOrder, err er // if number of ticks = 2, than the tickSpread is 0.02 // tickSpread = min(0.02 - 0.01, 0.02) // price = first bid price 28.00 + tickSpread (0.01) = 28.01 - tickSize := fixedpoint.NewFromFloat(e.market.TickSize) - tickSpread := tickSize.MulInt(e.NumOfTicks) - if spread > tickSize { + tickSize := e.market.TickSize + tickSpread := tickSize.Mul(fixedpoint.NewFromInt(int64(e.NumOfTicks))) + if spread.Compare(tickSize) > 0 { // there is a gap in the spread - tickSpread = fixedpoint.Min(tickSpread, spread-tickSize) + tickSpread = fixedpoint.Min(tickSpread, spread.Sub(tickSize)) switch e.Side { case types.SideTypeSell: - newPrice -= tickSpread + newPrice = newPrice.Sub(tickSpread) case types.SideTypeBuy: - newPrice += tickSpread + newPrice = newPrice.Add(tickSpread) } } - if e.StopPrice > 0 { + if e.StopPrice.Sign() > 0 { switch e.Side { case types.SideTypeSell: - if newPrice < e.StopPrice { - log.Infof("%s order price %f is lower than the stop sell price %f, setting order price to the stop sell price %f", + if newPrice.Compare(e.StopPrice) < 0 { + log.Infof("%s order price %s is lower than the stop sell price %s, setting order price to the stop sell price %s", e.Symbol, - newPrice.Float64(), - e.StopPrice.Float64(), - e.StopPrice.Float64()) + newPrice.String(), + e.StopPrice.String(), + e.StopPrice.String()) newPrice = e.StopPrice } case types.SideTypeBuy: - if newPrice > e.StopPrice { - log.Infof("%s order price %f is higher than the stop buy price %f, setting order price to the stop buy price %f", + if newPrice.Compare(e.StopPrice) > 0 { + log.Infof("%s order price %s is higher than the stop buy price %s, setting order price to the stop buy price %s", e.Symbol, - newPrice.Float64(), - e.StopPrice.Float64(), - e.StopPrice.Float64()) + newPrice.String(), + e.StopPrice.String(), + e.StopPrice.String()) newPrice = e.StopPrice } } } - minQuantity := fixedpoint.NewFromFloat(e.market.MinQuantity) + minQuantity := e.market.MinQuantity base := e.position.GetBase() - restQuantity := e.TargetQuantity - fixedpoint.Abs(base) + restQuantity := e.TargetQuantity.Sub(base.Abs()) - if restQuantity <= 0 { + if restQuantity.Sign() <= 0 { if e.cancelContextIfTargetQuantityFilled() { return } } - if restQuantity < minQuantity { - return orderForm, fmt.Errorf("can not continue placing orders, rest quantity %f is less than the min quantity %f", restQuantity.Float64(), minQuantity.Float64()) + if restQuantity.Compare(minQuantity) < 0 { + return orderForm, fmt.Errorf("can not continue placing orders, rest quantity %s is less than the min quantity %s", restQuantity.String(), minQuantity.String()) } // when slice = 1000, if we only have 998, we should adjust our quantity to 998 @@ -150,12 +150,12 @@ func (e *TwapExecution) newBestPriceOrder() (orderForm types.SubmitOrder, err er // if the rest quantity in the next round is not enough, we should merge the rest quantity into this round // if there are rest slices - nextRestQuantity := restQuantity - e.SliceQuantity - if nextRestQuantity > 0 && nextRestQuantity < minQuantity { + nextRestQuantity := restQuantity.Sub(e.SliceQuantity) + if nextRestQuantity.Sign() > 0 && nextRestQuantity.Compare(minQuantity) < 0 { orderQuantity = restQuantity } - minNotional := fixedpoint.NewFromFloat(e.market.MinNotional) + minNotional := e.market.MinNotional orderQuantity = AdjustQuantityByMinAmount(orderQuantity, newPrice, minNotional) switch e.Side { @@ -179,7 +179,7 @@ func (e *TwapExecution) newBestPriceOrder() (orderForm types.SubmitOrder, err er Symbol: e.Symbol, Side: e.Side, Type: types.OrderTypeMarket, - Quantity: restQuantity.Float64(), + Quantity: restQuantity, Market: e.market, } return orderForm, nil @@ -191,8 +191,8 @@ func (e *TwapExecution) newBestPriceOrder() (orderForm types.SubmitOrder, err er Symbol: e.Symbol, Side: e.Side, Type: types.OrderTypeLimitMaker, - Quantity: orderQuantity.Float64(), - Price: newPrice.Float64(), + Quantity: orderQuantity, + Price: newPrice, Market: e.market, TimeInForce: "GTC", } @@ -214,8 +214,9 @@ func (e *TwapExecution) updateOrder(ctx context.Context) error { return fmt.Errorf("no secoond price on the %s order book %s, can not update", e.Symbol, e.Side) } - tickSize := fixedpoint.NewFromFloat(e.market.TickSize) - tickSpread := tickSize.MulInt(e.NumOfTicks) + tickSize := e.market.TickSize + numOfTicks := fixedpoint.NewFromInt(int64(e.NumOfTicks)) + tickSpread := tickSize.Mul(numOfTicks) // check and see if we need to cancel the existing active orders for e.activeMakerOrders.NumOfOrders() > 0 { @@ -227,12 +228,12 @@ func (e *TwapExecution) updateOrder(ctx context.Context) error { // get the first order order := orders[0] - orderPrice := fixedpoint.NewFromFloat(order.Price) + orderPrice := order.Price // quantity := fixedpoint.NewFromFloat(order.Quantity) - remainingQuantity := order.Quantity - order.ExecutedQuantity - if remainingQuantity <= e.market.MinQuantity { - log.Infof("order remaining quantity %f is less than the market minimal quantity %f, skip updating order", remainingQuantity, e.market.MinQuantity) + remainingQuantity := order.Quantity.Sub(order.ExecutedQuantity) + if remainingQuantity.Compare(e.market.MinQuantity) <= 0 { + log.Infof("order remaining quantity %s is less than the market minimal quantity %s, skip updating order", remainingQuantity.String(), e.market.MinQuantity.String()) return nil } @@ -241,24 +242,24 @@ func (e *TwapExecution) updateOrder(ctx context.Context) error { // DO NOT UPDATE IF: // tickSpread > 0 AND current order price == second price + tickSpread // current order price == first price - log.Infof("orderPrice = %f first.Price = %f second.Price = %f tickSpread = %f", orderPrice.Float64(), first.Price.Float64(), second.Price.Float64(), tickSpread.Float64()) + log.Infof("orderPrice = %s first.Price = %s second.Price = %s tickSpread = %s", orderPrice.String(), first.Price.String(), second.Price.String(), tickSpread.String()) switch e.Side { case types.SideTypeBuy: - if tickSpread > 0 && orderPrice == second.Price+tickSpread { - log.Infof("the current order is already on the best ask price %f", orderPrice.Float64()) + if tickSpread.Sign() > 0 && orderPrice == second.Price.Add(tickSpread) { + log.Infof("the current order is already on the best ask price %s", orderPrice.String()) return nil } else if orderPrice == first.Price { - log.Infof("the current order is already on the best bid price %f", orderPrice.Float64()) + log.Infof("the current order is already on the best bid price %s", orderPrice.String()) return nil } case types.SideTypeSell: - if tickSpread > 0 && orderPrice == second.Price-tickSpread { - log.Infof("the current order is already on the best ask price %f", orderPrice.Float64()) + if tickSpread.Sign() > 0 && orderPrice == second.Price.Sub(tickSpread) { + log.Infof("the current order is already on the best ask price %s", orderPrice.String()) return nil } else if orderPrice == first.Price { - log.Infof("the current order is already on the best ask price %f", orderPrice.Float64()) + log.Infof("the current order is already on the best ask price %s", orderPrice.String()) return nil } } @@ -340,7 +341,7 @@ func (e *TwapExecution) orderUpdater(ctx context.Context) { func (e *TwapExecution) cancelContextIfTargetQuantityFilled() bool { base := e.position.GetBase() - if fixedpoint.Abs(base) >= e.TargetQuantity { + if base.Abs().Compare(e.TargetQuantity) >= 0 { log.Infof("filled target quantity, canceling the order execution context") e.cancelExecution() return true diff --git a/pkg/server/routes.go b/pkg/server/routes.go index 5d396cc51..240ecd652 100644 --- a/pkg/server/routes.go +++ b/pkg/server/routes.go @@ -418,17 +418,17 @@ func genFakeAssets() types.AssetMap { "MAX": types.Balance{Currency: "MAX", Available: fixedpoint.NewFromFloat(200000.0 * rand.Float64())}, "COMP": types.Balance{Currency: "COMP", Available: fixedpoint.NewFromFloat(100.0 * rand.Float64())}, } - assets := balances.Assets(map[string]float64{ - "BTCUSDT": 38000.0, - "BCHUSDT": 478.0, - "LTCUSDT": 150.0, - "COMPUSDT": 450.0, - "ETHUSDT": 1700.0, - "BNBUSDT": 70.0, - "GRTUSDT": 0.89, - "DOTUSDT": 20.0, - "SANDUSDT": 0.13, - "MAXUSDT": 0.122, + assets := balances.Assets(map[string]fixedpoint.Value{ + "BTCUSDT": fixedpoint.NewFromFloat(38000.0), + "BCHUSDT": fixedpoint.NewFromFloat(478.0), + "LTCUSDT": fixedpoint.NewFromFloat(150.0), + "COMPUSDT": fixedpoint.NewFromFloat(450.0), + "ETHUSDT": fixedpoint.NewFromFloat(1700.0), + "BNBUSDT": fixedpoint.NewFromFloat(70.0), + "GRTUSDT": fixedpoint.NewFromFloat(0.89), + "DOTUSDT": fixedpoint.NewFromFloat(20.0), + "SANDUSDT": fixedpoint.NewFromFloat(0.13), + "MAXUSDT": fixedpoint.NewFromFloat(0.122), }) for currency, asset := range assets { totalAssets[currency] = asset