From e221f543973a0ff401ac0ff8287babe26e5177e5 Mon Sep 17 00:00:00 2001 From: zenix Date: Wed, 2 Feb 2022 20:37:18 +0900 Subject: [PATCH] add dnum as the fixedpoint implementation. change types float64 to fixedpoint.Value change pnl report to use fixedpoint fix: migrate kline to use fixedpoint --- pkg/accounting/cost_distribution.go | 58 +- pkg/accounting/pnl/avg_cost.go | 29 +- pkg/accounting/pnl/report.go | 38 +- pkg/bbgo/scale.go | 4 +- pkg/bbgo/scale_test.go | 47 +- pkg/bbgo/session.go | 8 +- pkg/exchange/okex/exchange.go | 12 +- pkg/exchange/okex/parse.go | 12 +- pkg/fixedpoint/convert.go | 369 ------- pkg/fixedpoint/dec.go | 985 ++++++++++++++++++ .../{convert_test.go => dec_test.go} | 28 +- pkg/fixedpoint/div128.go | 132 +++ pkg/indicator/obv.go | 5 +- pkg/strategy/grid/strategy.go | 2 +- pkg/types/account.go | 76 +- pkg/types/kline.go | 254 +++-- pkg/types/kline_test.go | 35 +- pkg/types/position.go | 136 +-- pkg/types/position_test.go | 73 +- pkg/types/price_volume_heartbeat.go | 2 +- pkg/types/price_volume_slice.go | 39 +- pkg/types/rbtorderbook.go | 18 +- pkg/types/rbtree.go | 10 +- pkg/types/sliceorderbook.go | 14 +- pkg/types/trade.go | 40 +- pkg/util/math.go | 5 + 26 files changed, 1619 insertions(+), 812 deletions(-) delete mode 100644 pkg/fixedpoint/convert.go create mode 100644 pkg/fixedpoint/dec.go rename pkg/fixedpoint/{convert_test.go => dec_test.go} (90%) create mode 100644 pkg/fixedpoint/div128.go diff --git a/pkg/accounting/cost_distribution.go b/pkg/accounting/cost_distribution.go index 83eefe0c69..cedf7666b2 100644 --- a/pkg/accounting/cost_distribution.go +++ b/pkg/accounting/cost_distribution.go @@ -9,6 +9,7 @@ import ( "sync" "github.com/c9s/bbgo/pkg/types" + "github.com/c9s/bbgo/pkg/fixedpoint" ) func zero(a float64) bool { @@ -25,30 +26,30 @@ func (stock *Stock) String() string { return fmt.Sprintf("%f (%f)", stock.Price, stock.Quantity) } -func (stock *Stock) Consume(quantity float64) float64 { - q := math.Min(stock.Quantity, quantity) - stock.Quantity = round(stock.Quantity - q) +func (stock *Stock) Consume(quantity fixedpoint.Value) fixedpoint.Value { + q := fixedpoint.Min(stock.Quantity, quantity) + stock.Quantity = stock.Quantity.Sub(q).Round(0, fixedpoint.Down) return q } type StockSlice []Stock -func (slice StockSlice) QuantityBelowPrice(price float64) (quantity float64) { +func (slice StockSlice) QuantityBelowPrice(price fixedpoint.Value) (quantity fixedpoint.Value) { for _, stock := range slice { - if stock.Price < price { - quantity += stock.Quantity + if stock.Price.Compare(price) < 0 { + quantity = quantity.Add(stock.Quantity) } } - return round(quantity) + return quantity.Round(0, fixedpoint.Down) } -func (slice StockSlice) Quantity() (total float64) { +func (slice StockSlice) Quantity() (total fixedpoint.Value) { for _, stock := range slice { - total += stock.Quantity + total = total.Add(stock.Quantity) } - return round(total) + return total.Round(0, fixedpoint.Down) } type StockDistribution struct { @@ -62,27 +63,28 @@ type StockDistribution struct { type DistributionStats struct { PriceLevels []string `json:"priceLevels"` - TotalQuantity float64 `json:"totalQuantity"` - Quantities map[string]float64 `json:"quantities"` + TotalQuantity fixedpoint.Value `json:"totalQuantity"` + Quantities map[string]fixedpoint.Value `json:"quantities"` Stocks map[string]StockSlice `json:"stocks"` } func (m *StockDistribution) DistributionStats(level int) *DistributionStats { var d = DistributionStats{ - Quantities: map[string]float64{}, + Quantities: map[string]fixedpoint.Value {}, Stocks: map[string]StockSlice{}, } for _, stock := range m.Stocks { - n := math.Ceil(math.Log10(stock.Price)) + n := math.Ceil(math.Log10(stock.Price.Float64())) digits := int(n - math.Max(float64(level), 1.0)) + // TODO: use Round function in fixedpoint div := math.Pow10(digits) - priceLevel := math.Floor(stock.Price/div) * div + priceLevel := math.Floor(stock.Price.Float64()/div) * div key := strconv.FormatFloat(priceLevel, 'f', 2, 64) - d.TotalQuantity += stock.Quantity + d.TotalQuantity = d.TotalQuantity.Add(stock.Quantity) d.Stocks[key] = append(d.Stocks[key], stock) - d.Quantities[key] += stock.Quantity + d.Quantities[key] = d.Quantities[key].Add(stock.Quantity) } var priceLevels []float64 @@ -114,7 +116,7 @@ func (m *StockDistribution) squash() { var squashed StockSlice for _, stock := range m.Stocks { - if !zero(stock.Quantity) { + if !stock.Quantity.IsZero() { squashed = append(squashed, stock) } } @@ -152,11 +154,11 @@ func (m *StockDistribution) consume(sell Stock) error { stock := m.Stocks[idx] // find any stock price is lower than the sell trade - if stock.Price >= sell.Price { + if stock.Price.Compare(sell.Price) >= 0 { continue } - if zero(stock.Quantity) { + if stock.Quantity.IsZero() { continue } @@ -164,7 +166,7 @@ func (m *StockDistribution) consume(sell Stock) error { sell.Consume(delta) m.Stocks[idx] = stock - if zero(sell.Quantity) { + if sell.Quantity.IsZero() { return nil } } @@ -173,7 +175,7 @@ func (m *StockDistribution) consume(sell Stock) error { for ; idx >= 0; idx-- { stock := m.Stocks[idx] - if zero(stock.Quantity) { + if stock.Quantity.IsZero() { continue } @@ -181,12 +183,12 @@ func (m *StockDistribution) consume(sell Stock) error { sell.Consume(delta) m.Stocks[idx] = stock - if zero(sell.Quantity) { + if sell.Quantity.IsZero() { return nil } } - if sell.Quantity > 0.0 { + if sell.Quantity.Sign() > 0 { m.PendingSells = append(m.PendingSells, sell) } @@ -203,7 +205,7 @@ func (m *StockDistribution) AddTrades(trades []types.Trade) (checkpoints []int, trade.Symbol = m.Symbol trade.IsBuyer = false trade.Quantity = trade.Fee - trade.Fee = 0.0 + trade.Fee = fixedpoint.Zero } } @@ -238,11 +240,11 @@ func (m *StockDistribution) AddTrades(trades []types.Trade) (checkpoints []int, func toStock(trade types.Trade) Stock { if strings.HasPrefix(trade.Symbol, trade.FeeCurrency) { if trade.IsBuyer { - trade.Quantity -= trade.Fee + trade.Quantity = trade.Quantity.Sub(trade.Fee) } else { - trade.Quantity += trade.Fee + trade.Quantity = trade.Quantity.Add(trade.Fee) } - trade.Fee = 0.0 + trade.Fee = fixedpoint.Zero } return Stock(trade) } diff --git a/pkg/accounting/pnl/avg_cost.go b/pkg/accounting/pnl/avg_cost.go index ab4cfba68d..7db141398c 100644 --- a/pkg/accounting/pnl/avg_cost.go +++ b/pkg/accounting/pnl/avg_cost.go @@ -14,11 +14,11 @@ type AverageCostCalculator struct { Market types.Market } -func (c *AverageCostCalculator) Calculate(symbol string, trades []types.Trade, currentPrice float64) *AverageCostPnlReport { +func (c *AverageCostCalculator) Calculate(symbol string, trades []types.Trade, currentPrice fixedpoint.Value) *AverageCostPnlReport { // copy trades, so that we can truncate it. - var bidVolume = 0.0 - var askVolume = 0.0 - var feeUSD = 0.0 + var bidVolume = fixedpoint.Zero + var askVolume = fixedpoint.Zero + var feeUSD = fixedpoint.Zero if len(trades) == 0 { return &AverageCostPnlReport{ @@ -32,7 +32,7 @@ func (c *AverageCostCalculator) Calculate(symbol string, trades []types.Trade, c } } - var currencyFees = map[string]float64{} + var currencyFees = map[string]fixedpoint.Value{} var position = types.NewPositionFromMarket(c.Market) position.SetFeeRate(types.ExchangeFee{ @@ -60,26 +60,27 @@ func (c *AverageCostCalculator) Calculate(symbol string, trades []types.Trade, c profit, netProfit, madeProfit := position.AddTrade(trade) if madeProfit { - totalProfit += profit - totalNetProfit += netProfit + totalProfit = totalProfit.Add(profit) + totalNetProfit = totalNetProfit.Add(netProfit) } if trade.IsBuyer { - bidVolume += trade.Quantity + bidVolume = bidVolume.Add(trade.Quantity) } else { - askVolume += trade.Quantity + askVolume = askVolume.Add(trade.Quantity) } if _, ok := currencyFees[trade.FeeCurrency]; !ok { currencyFees[trade.FeeCurrency] = trade.Fee } else { - currencyFees[trade.FeeCurrency] += trade.Fee + currencyFees[trade.FeeCurrency] = currencyFees[trade.FeeCurrency].Add(trade.Fee) } tradeIDs[trade.ID] = trade } - unrealizedProfit := (fixedpoint.NewFromFloat(currentPrice) - position.AverageCost).Mul(position.GetBase()) + unrealizedProfit := currentPrice.Sub(position.AverageCost). + Mul(position.GetBase()) return &AverageCostPnlReport{ Symbol: symbol, Market: c.Market, @@ -90,12 +91,12 @@ func (c *AverageCostCalculator) Calculate(symbol string, trades []types.Trade, c BuyVolume: bidVolume, SellVolume: askVolume, - Stock: position.GetBase().Float64(), + Stock: position.GetBase(), Profit: totalProfit, NetProfit: totalNetProfit, UnrealizedProfit: unrealizedProfit, - AverageCost: position.AverageCost.Float64(), - FeeInUSD: (totalProfit - totalNetProfit).Float64(), + AverageCost: position.AverageCost, + FeeInUSD: totalProfit.Sub(totalNetProfit), CurrencyFees: currencyFees, } } diff --git a/pkg/accounting/pnl/report.go b/pkg/accounting/pnl/report.go index b9e7e56f9a..46ebad08d3 100644 --- a/pkg/accounting/pnl/report.go +++ b/pkg/accounting/pnl/report.go @@ -14,7 +14,7 @@ import ( ) type AverageCostPnlReport struct { - LastPrice float64 `json:"lastPrice"` + LastPrice fixedpoint.Value `json:"lastPrice"` StartTime time.Time `json:"startTime"` Symbol string `json:"symbol"` Market types.Market `json:"market"` @@ -23,12 +23,12 @@ type AverageCostPnlReport struct { Profit fixedpoint.Value `json:"profit"` NetProfit fixedpoint.Value `json:"netProfit"` UnrealizedProfit fixedpoint.Value `json:"unrealizedProfit"` - AverageCost float64 `json:"averageCost"` - BuyVolume float64 `json:"buyVolume,omitempty"` - SellVolume float64 `json:"sellVolume,omitempty"` - FeeInUSD float64 `json:"feeInUSD"` - Stock float64 `json:"stock"` - CurrencyFees map[string]float64 `json:"currencyFees"` + AverageCost fixedpoint.Value `json:"averageCost"` + BuyVolume fixedpoint.Value `json:"buyVolume,omitempty"` + SellVolume fixedpoint.Value `json:"sellVolume,omitempty"` + FeeInUSD fixedpoint.Value `json:"feeInUSD"` + Stock fixedpoint.Value `json:"stock"` + CurrencyFees map[string]fixedpoint.Value `json:"currencyFees"` } func (report *AverageCostPnlReport) JSON() ([]byte, error) { @@ -38,26 +38,26 @@ func (report *AverageCostPnlReport) JSON() ([]byte, error) { func (report AverageCostPnlReport) Print() { log.Infof("TRADES SINCE: %v", report.StartTime) log.Infof("NUMBER OF TRADES: %d", report.NumTrades) - log.Infof("AVERAGE COST: %s", types.USD.FormatMoneyFloat64(report.AverageCost)) - log.Infof("TOTAL BUY VOLUME: %f", report.BuyVolume) - log.Infof("TOTAL SELL VOLUME: %f", report.SellVolume) - log.Infof("STOCK: %f", report.Stock) + log.Infof("AVERAGE COST: %s", types.USD.FormatMoney(report.AverageCost)) + log.Infof("TOTAL BUY VOLUME: %s", report.BuyVolume.String()) + log.Infof("TOTAL SELL VOLUME: %s", report.SellVolume.String()) + log.Infof("STOCK: %s", report.Stock.String()) // FIXME: // log.Infof("FEE (USD): %f", report.FeeInUSD) - log.Infof("CURRENT PRICE: %s", types.USD.FormatMoneyFloat64(report.LastPrice)) + log.Infof("CURRENT PRICE: %s", types.USD.FormatMoney(report.LastPrice)) log.Infof("CURRENCY FEES:") for currency, fee := range report.CurrencyFees { - log.Infof(" - %s: %f", currency, fee) + log.Infof(" - %s: %s", currency, fee.String()) } - log.Infof("PROFIT: %s", types.USD.FormatMoneyFloat64(report.Profit.Float64())) - log.Infof("UNREALIZED PROFIT: %s", types.USD.FormatMoneyFloat64(report.UnrealizedProfit.Float64())) + log.Infof("PROFIT: %s", types.USD.FormatMoney(report.Profit)) + log.Infof("UNREALIZED PROFIT: %s", types.USD.FormatMoney(report.UnrealizedProfit)) } func (report AverageCostPnlReport) SlackAttachment() slack.Attachment { var color = slackstyle.Red - if report.UnrealizedProfit > 0 { + if report.UnrealizedProfit.Sign() > 0 { color = slackstyle.Green } @@ -70,12 +70,12 @@ func (report AverageCostPnlReport) SlackAttachment() slack.Attachment { Fields: []slack.AttachmentField{ {Title: "Profit", Value: types.USD.FormatMoney(report.Profit)}, {Title: "Unrealized Profit", Value: types.USD.FormatMoney(report.UnrealizedProfit)}, - {Title: "Current Price", Value: report.Market.FormatPrice(report.LastPrice), Short: true}, - {Title: "Average Cost", Value: report.Market.FormatPrice(report.AverageCost), Short: true}, + {Title: "Current Price", Value: report.Market.FormatPrice(report.LastPrice.Float64()), Short: true}, + {Title: "Average Cost", Value: report.Market.FormatPrice(report.AverageCost.Float64()), Short: true}, // FIXME: // {Title: "Fee (USD)", Value: types.USD.FormatMoney(report.FeeInUSD), Short: true}, - {Title: "Stock", Value: strconv.FormatFloat(report.Stock, 'f', 8, 64), Short: true}, + {Title: "Stock", Value: report.Stock.String(), Short: true}, {Title: "Number of Trades", Value: strconv.Itoa(report.NumTrades), Short: true}, }, Footer: report.StartTime.Format(time.RFC822), diff --git a/pkg/bbgo/scale.go b/pkg/bbgo/scale.go index 6a3fa67e85..ec4064b3e1 100644 --- a/pkg/bbgo/scale.go +++ b/pkg/bbgo/scale.go @@ -37,6 +37,7 @@ type ExponentialScale struct { a float64 b float64 h float64 + s float64 } func (s *ExponentialScale) Solve() error { @@ -51,6 +52,7 @@ func (s *ExponentialScale) Solve() error { s.h = s.Domain[0] s.a = s.Range[0] s.b = math.Pow(s.Range[1]/s.Range[0], 1/(s.Domain[1]-s.h)) + s.s = s.Domain[1] - s.h return nil } @@ -73,7 +75,7 @@ func (s *ExponentialScale) Call(x float64) (y float64) { x = s.Domain[1] } - y = s.a * math.Pow(s.b, x-s.h) + y = s.a * math.Pow(s.Range[1]/s.Range[0], (x-s.h)/s.s) return y } diff --git a/pkg/bbgo/scale_test.go b/pkg/bbgo/scale_test.go index ad48f0d2b1..2626113d70 100644 --- a/pkg/bbgo/scale_test.go +++ b/pkg/bbgo/scale_test.go @@ -19,8 +19,8 @@ func TestExponentialScale(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "f(x) = 0.001000 * 1.002305 ^ (x - 1000.000000)", scale.String()) - assert.Equal(t, fixedpoint.NewFromFloat(0.001), fixedpoint.NewFromFloat(scale.Call(1000.0))) - assert.Equal(t, fixedpoint.NewFromFloat(0.01), fixedpoint.NewFromFloat(scale.Call(2000.0))) + assert.True(t, fixedpoint.CmpEq(fixedpoint.NewFromFloat(0.001), fixedpoint.NewFromFloat(scale.Call(1000.0)))) + assert.True(t, fixedpoint.CmpEq(fixedpoint.NewFromFloat(0.01), fixedpoint.NewFromFloat(scale.Call(2000.0)))) for x := 1000; x <= 2000; x += 100 { y := scale.Call(float64(x)) @@ -38,8 +38,8 @@ func TestExponentialScale_Reverse(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "f(x) = 0.100000 * 0.995405 ^ (x - 1000.000000)", scale.String()) - assert.Equal(t, fixedpoint.NewFromFloat(0.1), fixedpoint.NewFromFloat(scale.Call(1000.0))) - assert.Equal(t, fixedpoint.NewFromFloat(0.001), fixedpoint.NewFromFloat(scale.Call(2000.0))) + assert.True(t, fixedpoint.CmpEq(fixedpoint.NewFromFloat(0.1), fixedpoint.NewFromFloat(scale.Call(1000.0)))) + assert.True(t, fixedpoint.CmpEq(fixedpoint.NewFromFloat(0.001), fixedpoint.NewFromFloat(scale.Call(2000.0)))) for x := 1000; x <= 2000; x += 100 { y := scale.Call(float64(x)) @@ -57,8 +57,8 @@ func TestLogScale(t *testing.T) { err := scale.Solve() assert.NoError(t, err) assert.Equal(t, "f(x) = 0.001303 * log(x - 999.000000) + 0.001000", scale.String()) - assert.Equal(t, fixedpoint.NewFromFloat(0.001), fixedpoint.NewFromFloat(scale.Call(1000.0))) - assert.Equal(t, fixedpoint.NewFromFloat(0.01), fixedpoint.NewFromFloat(scale.Call(2000.0))) + assert.True(t, fixedpoint.CmpEqDelta(fixedpoint.NewFromFloat(0.001), fixedpoint.NewFromFloat(scale.Call(1000.0)), 1e-9)) + assert.True(t, fixedpoint.CmpEqDelta(fixedpoint.NewFromFloat(0.01), fixedpoint.NewFromFloat(scale.Call(2000.0)), 1e-9)) for x := 1000; x <= 2000; x += 100 { y := scale.Call(float64(x)) t.Logf("%s = %f", scale.FormulaOf(float64(x)), y) @@ -74,8 +74,8 @@ 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, fixedpoint.NewFromFloat(3), fixedpoint.NewFromFloat(scale.Call(1000))) - assert.Equal(t, fixedpoint.NewFromFloat(10), fixedpoint.NewFromFloat(scale.Call(2000))) + assert.True(t, fixedpoint.CmpEq(fixedpoint.NewFromFloat(3), fixedpoint.NewFromFloat(scale.Call(1000)))) + assert.True(t, fixedpoint.CmpEq(fixedpoint.NewFromFloat(10), fixedpoint.NewFromFloat(scale.Call(2000)))) for x := 1000; x <= 2000; x += 100 { y := scale.Call(float64(x)) t.Logf("%s = %f", scale.FormulaOf(float64(x)), y) @@ -91,8 +91,8 @@ 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, fixedpoint.NewFromFloat(0.1), fixedpoint.NewFromFloat(scale.Call(1))) - assert.Equal(t, fixedpoint.NewFromFloat(0.4), fixedpoint.NewFromFloat(scale.Call(3))) + assert.True(t, fixedpoint.CmpEq(fixedpoint.NewFromFloat(0.1), fixedpoint.NewFromFloat(scale.Call(1)))) + assert.True(t, fixedpoint.CmpEq(fixedpoint.NewFromFloat(0.4), fixedpoint.NewFromFloat(scale.Call(3)))) } func TestQuadraticScale(t *testing.T) { @@ -105,9 +105,9 @@ func TestQuadraticScale(t *testing.T) { err := scale.Solve() assert.NoError(t, err) assert.Equal(t, "f(x) = 0.000550 * x ^ 2 + 0.135000 * x + 1.000000", scale.String()) - assert.Equal(t, fixedpoint.NewFromFloat(1), fixedpoint.NewFromFloat(scale.Call(0))) - assert.Equal(t, fixedpoint.NewFromFloat(20), fixedpoint.NewFromFloat(scale.Call(100.0))) - assert.Equal(t, fixedpoint.NewFromFloat(50.0), fixedpoint.NewFromFloat(scale.Call(200.0))) + assert.True(t, fixedpoint.CmpEq(fixedpoint.NewFromFloat(1), fixedpoint.NewFromFloat(scale.Call(0)))) + assert.True(t, fixedpoint.CmpEq(fixedpoint.NewFromFloat(20), fixedpoint.NewFromFloat(scale.Call(100.0)))) + assert.True(t, fixedpoint.CmpEq(fixedpoint.NewFromFloat(50.0), fixedpoint.NewFromFloat(scale.Call(200.0)))) for x := 0; x <= 200; x += 1 { y := scale.Call(float64(x)) t.Logf("%s = %f", scale.FormulaOf(float64(x)), y) @@ -127,11 +127,11 @@ func TestPercentageScale(t *testing.T) { v, err := s.Scale(0.0) assert.NoError(t, err) - assert.Equal(t, fixedpoint.NewFromFloat(1.0), fixedpoint.NewFromFloat(v)) + assert.True(t, fixedpoint.CmpEq(fixedpoint.NewFromFloat(1.0), fixedpoint.NewFromFloat(v))) v, err = s.Scale(1.0) assert.NoError(t, err) - assert.Equal(t, fixedpoint.NewFromFloat(100.0), fixedpoint.NewFromFloat(v)) + assert.True(t, fixedpoint.CmpEq(fixedpoint.NewFromFloat(100.0), fixedpoint.NewFromFloat(v))) }) t.Run("from -1.0 to 1.0", func(t *testing.T) { @@ -146,11 +146,11 @@ func TestPercentageScale(t *testing.T) { v, err := s.Scale(-1.0) assert.NoError(t, err) - assert.Equal(t, fixedpoint.NewFromFloat(10.0), fixedpoint.NewFromFloat(v)) + assert.True(t, fixedpoint.CmpEq(fixedpoint.NewFromFloat(10.0), fixedpoint.NewFromFloat(v))) v, err = s.Scale(1.0) assert.NoError(t, err) - assert.Equal(t, fixedpoint.NewFromFloat(100.0), fixedpoint.NewFromFloat(v)) + assert.True(t, fixedpoint.CmpEq(fixedpoint.NewFromFloat(100.0), fixedpoint.NewFromFloat(v))) }) t.Run("reverse -1.0 to 1.0", func(t *testing.T) { @@ -165,19 +165,19 @@ func TestPercentageScale(t *testing.T) { v, err := s.Scale(-1.0) assert.NoError(t, err) - assert.Equal(t, fixedpoint.NewFromFloat(100.0), fixedpoint.NewFromFloat(v)) + assert.True(t, fixedpoint.CmpEq(fixedpoint.NewFromFloat(100.0), fixedpoint.NewFromFloat(v))) v, err = s.Scale(1.0) assert.NoError(t, err) - assert.Equal(t, fixedpoint.NewFromFloat(10.0), fixedpoint.NewFromFloat(v)) + assert.True(t, fixedpoint.CmpEq(fixedpoint.NewFromFloat(10.0), fixedpoint.NewFromFloat(v))) v, err = s.Scale(2.0) assert.NoError(t, err) - assert.Equal(t, fixedpoint.NewFromFloat(10.0), fixedpoint.NewFromFloat(v)) + assert.True(t, fixedpoint.CmpEq(fixedpoint.NewFromFloat(10.0), fixedpoint.NewFromFloat(v))) v, err = s.Scale(-2.0) assert.NoError(t, err) - assert.Equal(t, fixedpoint.NewFromFloat(100.0), fixedpoint.NewFromFloat(v)) + assert.True(t, fixedpoint.CmpEq(fixedpoint.NewFromFloat(100.0), fixedpoint.NewFromFloat(v))) }) t.Run("negative range", func(t *testing.T) { @@ -192,11 +192,10 @@ func TestPercentageScale(t *testing.T) { v, err := s.Scale(0.0) assert.NoError(t, err) - assert.Equal(t, fixedpoint.NewFromFloat(-100.0), fixedpoint.NewFromFloat(v)) + assert.True(t, fixedpoint.CmpEq(fixedpoint.NewFromFloat(-100.0), fixedpoint.NewFromFloat(v))) v, err = s.Scale(1.0) assert.NoError(t, err) - assert.Equal(t, fixedpoint.NewFromFloat(100.0), fixedpoint.NewFromFloat(v)) + assert.True(t, fixedpoint.CmpEq(fixedpoint.NewFromFloat(100.0), fixedpoint.NewFromFloat(v))) }) } - diff --git a/pkg/bbgo/session.go b/pkg/bbgo/session.go index 91b8f7b20a..0c257997c7 100644 --- a/pkg/bbgo/session.go +++ b/pkg/bbgo/session.go @@ -217,9 +217,9 @@ type ExchangeSession struct { orderBooks map[string]*types.StreamOrderBook // startPrices is used for backtest - startPrices map[string]float64 + startPrices map[string]fixedpoint.Value - lastPrices map[string]float64 + lastPrices map[string]fixedpoint.Value lastPriceUpdatedAt time.Time // marketDataStores contains the market data store of each market @@ -260,8 +260,8 @@ func NewExchangeSession(name string, exchange types.Exchange) *ExchangeSession { orderBooks: make(map[string]*types.StreamOrderBook), markets: make(map[string]types.Market), - startPrices: make(map[string]float64), - lastPrices: make(map[string]float64), + startPrices: make(map[string]fixedpoint.Value), + lastPrices: make(map[string]fixedpoint.Value), positions: make(map[string]*types.Position), marketDataStores: make(map[string]*MarketDataStore), standardIndicatorSets: make(map[string]*StandardIndicatorSet), diff --git a/pkg/exchange/okex/exchange.go b/pkg/exchange/okex/exchange.go index d230974a44..2368fd812a 100644 --- a/pkg/exchange/okex/exchange.go +++ b/pkg/exchange/okex/exchange.go @@ -291,13 +291,13 @@ func (e *Exchange) QueryKLines(ctx context.Context, symbol string, interval type Exchange: types.ExchangeOKEx, Symbol: symbol, Interval: interval, - Open: candle.Open.Float64(), - High: candle.High.Float64(), - Low: candle.Low.Float64(), - Close: candle.Close.Float64(), + Open: candle.Open, + High: candle.High, + Low: candle.Low, + Close: candle.Close, Closed: true, - Volume: candle.Volume.Float64(), - QuoteVolume: candle.VolumeInCurrency.Float64(), + Volume: candle.Volume, + QuoteVolume: candle.VolumeInCurrency, StartTime: types.Time(candle.Time), EndTime: types.Time(candle.Time.Add(interval.Duration() - time.Millisecond)), }) diff --git a/pkg/exchange/okex/parse.go b/pkg/exchange/okex/parse.go index ba955bce37..6b9c019ed1 100644 --- a/pkg/exchange/okex/parse.go +++ b/pkg/exchange/okex/parse.go @@ -209,12 +209,12 @@ func (c *Candle) KLine() types.KLine { return types.KLine{ Exchange: types.ExchangeOKEx, Interval: interval, - Open: c.Open.Float64(), - High: c.High.Float64(), - Low: c.Low.Float64(), - Close: c.Close.Float64(), - Volume: c.Volume.Float64(), - QuoteVolume: c.VolumeInCurrency.Float64(), + Open: c.Open, + High: c.High, + Low: c.Low, + Close: c.Close, + Volume: c.Volume, + QuoteVolume: c.VolumeInCurrency, StartTime: types.Time(c.StartTime), EndTime: types.Time(endTime), } diff --git a/pkg/fixedpoint/convert.go b/pkg/fixedpoint/convert.go deleted file mode 100644 index 3169b7cd57..0000000000 --- a/pkg/fixedpoint/convert.go +++ /dev/null @@ -1,369 +0,0 @@ -package fixedpoint - -import ( - "database/sql/driver" - "encoding/json" - "errors" - "fmt" - "math" - "math/big" - "strconv" - "sync/atomic" -) - -const MaxPrecision = 12 -const DefaultPrecision = 8 - -const DefaultPow = 1e8 - -type Value int64 - -func (v Value) Value() (driver.Value, error) { - return v.Float64(), nil -} - -func (v *Value) Scan(src interface{}) error { - switch d := src.(type) { - case int64: - *v = Value(d) - return nil - - case float64: - *v = NewFromFloat(d) - return nil - - case []byte: - vv, err := NewFromString(string(d)) - if err != nil { - return err - } - *v = vv - return nil - - default: - - } - - return fmt.Errorf("fixedpoint.Value scan error, type: %T is not supported, value; %+v", src, src) -} - -func (v Value) Float64() float64 { - return float64(v) / DefaultPow -} - -func (v Value) Abs() Value { - if v < 0 { - return -v - } - return v -} - -func (v Value) String() string { - return strconv.FormatFloat(float64(v)/DefaultPow, 'f', -1, 64) -} - -func (v Value) Percentage() string { - return fmt.Sprintf("%.2f%%", v.Float64()*100.0) -} - -func (v Value) SignedPercentage() string { - if v > 0 { - return "+" + v.Percentage() - } - return v.Percentage() -} - -func (v Value) Int64() int64 { - return int64(v.Float64()) -} - -func (v Value) Int() int { - return int(v.Float64()) -} - -// BigMul is the math/big version multiplication -func (v Value) BigMul(v2 Value) Value { - x := new(big.Int).Mul(big.NewInt(int64(v)), big.NewInt(int64(v2))) - return Value(x.Int64() / DefaultPow) -} - -func (v Value) Mul(v2 Value) Value { - return NewFromFloat(v.Float64() * v2.Float64()) -} - -func (v Value) MulInt(v2 int) Value { - return NewFromFloat(v.Float64() * float64(v2)) -} - -func (v Value) MulFloat64(v2 float64) Value { - return NewFromFloat(v.Float64() * v2) -} - -func (v Value) Div(v2 Value) Value { - return NewFromFloat(v.Float64() / v2.Float64()) -} - -func (v Value) DivFloat64(v2 float64) Value { - return NewFromFloat(v.Float64() / v2) -} - -func (v Value) Floor() Value { - return NewFromFloat(math.Floor(v.Float64())) -} - -func (v Value) Ceil() Value { - return NewFromFloat(math.Ceil(v.Float64())) -} - -func (v Value) Sub(v2 Value) Value { - return Value(int64(v) - int64(v2)) -} - -func (v Value) Add(v2 Value) Value { - return Value(int64(v) + int64(v2)) -} - -func (v *Value) AtomicAdd(v2 Value) { - atomic.AddInt64((*int64)(v), int64(v2)) -} - -func (v *Value) AtomicLoad() Value { - i := atomic.LoadInt64((*int64)(v)) - return Value(i) -} - -func (v *Value) UnmarshalYAML(unmarshal func(a interface{}) error) (err error) { - var f float64 - if err = unmarshal(&f); err == nil { - *v = NewFromFloat(f) - return - } - - var i int64 - if err = unmarshal(&i); err == nil { - *v = NewFromInt64(i) - return - } - - var s string - if err = unmarshal(&s); err == nil { - nv, err2 := NewFromString(s) - if err2 == nil { - *v = nv - return - } - } - - return err -} - -func (v Value) MarshalJSON() ([]byte, error) { - f := float64(v) / DefaultPow - o := strconv.FormatFloat(f, 'f', 8, 64) - return []byte(o), nil -} - -func (v *Value) UnmarshalJSON(data []byte) error { - var a interface{} - var err = json.Unmarshal(data, &a) - if err != nil { - return err - } - - switch d := a.(type) { - case float64: - *v = NewFromFloat(d) - - case float32: - *v = NewFromFloat32(d) - - case int: - *v = NewFromInt(d) - - case int64: - *v = NewFromInt64(d) - - case string: - v2, err := NewFromString(d) - if err != nil { - return err - } - - *v = v2 - - default: - return fmt.Errorf("unsupported type: %T %v", d, d) - - } - - return nil -} - -func Must(v Value, err error) Value { - if err != nil { - panic(err) - } - - return v -} - -var ErrPrecisionLoss = errors.New("precision loss") - -func Parse(input string) (num int64, numDecimalPoints int, err error) { - length := len(input) - isPercentage := input[length-1] == '%' - if isPercentage { - length -= 1 - input = input[0:length] - } - - var neg int64 = 1 - var digit int64 - for i := 0; i < length; i++ { - c := input[i] - if c == '-' { - neg = -1 - } else if c >= '0' && c <= '9' { - digit, err = strconv.ParseInt(string(c), 10, 64) - if err != nil { - return - } - - num = num*10 + digit - } else if c == '.' { - i++ - if i > len(input)-1 { - err = fmt.Errorf("expect fraction numbers after dot") - return - } - - for j := i; j < len(input); j++ { - fc := input[j] - if fc >= '0' && fc <= '9' { - digit, err = strconv.ParseInt(string(fc), 10, 64) - if err != nil { - return - } - - numDecimalPoints++ - num = num*10 + digit - - if numDecimalPoints >= MaxPrecision { - return num, numDecimalPoints, ErrPrecisionLoss - } - } else { - err = fmt.Errorf("expect digit, got %c", fc) - return - } - } - break - } else { - err = fmt.Errorf("unexpected char %c", c) - return - } - } - - num = num * neg - if isPercentage { - numDecimalPoints += 2 - } - - return num, numDecimalPoints, nil -} - -func NewFromAny(any interface{}) (Value, error) { - switch v := any.(type) { - case string: - return NewFromString(v) - case float64: - return NewFromFloat(v), nil - case int64: - return NewFromInt64(v), nil - - default: - return 0, fmt.Errorf("fixedpoint unsupported type %v", v) - } -} - -func NewFromString(input string) (Value, error) { - length := len(input) - - if length == 0 { - return 0, nil - } - - isPercentage := input[length-1] == '%' - if isPercentage { - input = input[0 : length-1] - } - - v, err := strconv.ParseFloat(input, 64) - if err != nil { - return 0, err - } - - if isPercentage { - v = v * 0.01 - } - - return NewFromFloat(v), nil -} - -func MustNewFromString(input string) Value { - v, err := NewFromString(input) - if err != nil { - panic(fmt.Errorf("can not parse %s into fixedpoint, error: %s", input, err.Error())) - } - return v -} - -func NewFromFloat(val float64) Value { - return Value(int64(math.Round(val * DefaultPow))) -} - -func NewFromFloat32(val float32) Value { - return Value(int64(math.Round(float64(val) * DefaultPow))) -} - -func NewFromInt(val int) Value { - return Value(int64(val * DefaultPow)) -} - -func NewFromInt64(val int64) Value { - return Value(val * DefaultPow) -} - -func NumFractionalDigits(a Value) int { - numPow := 0 - for pow := int64(DefaultPow); pow%10 != 1; pow /= 10 { - numPow++ - } - numZeros := 0 - for v := int64(a); v%10 == 0; v /= 10 { - numZeros++ - } - return numPow - numZeros -} - -func Min(a, b Value) Value { - if a < b { - return a - } - - return b -} - -func Max(a, b Value) Value { - if a > b { - return a - } - - return b -} - -func Abs(a Value) Value { - if a < 0 { - return -a - } - return a -} diff --git a/pkg/fixedpoint/dec.go b/pkg/fixedpoint/dec.go new file mode 100644 index 0000000000..e01de1c174 --- /dev/null +++ b/pkg/fixedpoint/dec.go @@ -0,0 +1,985 @@ +package fixedpoint + +import ( + "bytes" + "math" + "math/bits" + "strconv" + "strings" + "database/sql/driver" + "encoding/json" + "fmt" + "errors" +) + +type Value struct { + coef uint64 + sign int8 + exp int +} + +const ( + signPosInf = +2 + signPos = +1 + signZero = 0 + signNeg = -1 + signNegInf = -2 + expMin = math.MinInt16 + expMax = math.MaxInt16 + coefMin = 1000_0000_0000_0000 + coefMax = 9999_9999_9999_9999 + digitsMax = 16 + shiftMax = digitsMax - 1 +) + + +// common values +var ( + Zero = Value{} + One = Value{1000_0000_0000_0000, signPos, 1} + NegOne = Value{1000_0000_0000_0000, signNeg, 1} + PosInf = Value{1, signPosInf, 0} + NegInf = Value{1, signNegInf, 0} +) + +var pow10f = [...]float64{ + 1, + 10, + 100, + 1000, + 10000, + 100000, + 1000000, + 10000000, + 100000000, + 1000000000, + 10000000000, + 100000000000, + 1000000000000, + 10000000000000, + 100000000000000, + 1000000000000000, + 10000000000000000, + 100000000000000000, + 1000000000000000000, + 10000000000000000000, + 100000000000000000000} + +var pow10 = [...]uint64{ + 1, + 10, + 100, + 1000, + 10000, + 100000, + 1000000, + 10000000, + 100000000, + 1000000000, + 10000000000, + 100000000000, + 1000000000000, + 10000000000000, + 100000000000000, + 1000000000000000, + 10000000000000000, + 100000000000000000, + 1000000000000000000} + +var halfpow10 = [...]uint64{ + 0, + 5, + 50, + 500, + 5000, + 50000, + 500000, + 5000000, + 50000000, + 500000000, + 5000000000, + 50000000000, + 500000000000, + 5000000000000, + 50000000000000, + 500000000000000, + 5000000000000000, + 50000000000000000, + 500000000000000000, + 5000000000000000000} + +func (v Value) Value() (driver.Value, error) { + return v.Float64(), nil +} + +// NewFromInt returns a Value for an int +func NewFromInt(n int64) Value { + if n == 0 { + return Zero + } + //n0 := n + sign := int8(signPos) + if n < 0 { + n = -n + sign = signNeg + } + dn := New(sign, uint64(n), digitsMax) + //check(reversible(n0, dn)) + return dn +} + +func reversible(n int64, dn Value) bool { + n2 := dn.Int64() + return n2 == n +} + +const log2of10 = 3.32192809488736234 + +// NewFromFloat converts a float64 to a Value +func NewFromFloat(f float64) Value { + switch { + case math.IsInf(f, +1): + return PosInf + case math.IsInf(f, -1): + return NegInf + case math.IsNaN(f): + panic("value.NewFromFloat can't convert NaN") + } + + n := int64(f) + if f == float64(n) { + return NewFromInt(n) + } + + sign := int8(signPos) + if f < 0 { + f = -f + sign = signNeg + } + _, e := math.Frexp(f) + e = int(float32(e) / log2of10) + if e - 16 < 0 { + c := uint64(f * pow10f[16 - e]) + return New(sign, c, e) + } else { + c := uint64(f / pow10f[e - 16]) + return New(sign, c, e) + } +} + +// Raw constructs a Value without normalizing - arguments must be valid. +// Used by SuValue Unpack +func Raw(sign int8, coef uint64, exp int) Value { + return Value{coef, sign, int(exp)} +} + +// New constructs a Value, maximizing coef and handling exp out of range +// Used to normalize results of operations +func New(sign int8, coef uint64, exp int) Value { + if sign == 0 || coef == 0 { + return Zero + } else if sign == signPosInf { + return PosInf + } else if sign == signNegInf { + return NegInf + } else { + atmax := false + for coef > coefMax { + coef = (coef + 5) / 10 + exp++ + atmax = true + } + + if !atmax { + p := maxShift(coef) + coef *= pow10[p] + exp -= p + } + if exp > expMax { + return Inf(sign) + } + return Value{coef, sign, exp} + } +} + +func maxShift(x uint64) int { + i := ilog10(x) + if i > shiftMax { + return 0 + } + return shiftMax - i +} + +func ilog10(x uint64) int { + // based on Hacker's Delight + if x == 0 { + return 0 + } + y := (19 * (63 - bits.LeadingZeros64(x))) >> 6 + if y < 18 && x >= pow10[y+1] { + y++ + } + return y +} + +func Inf(sign int8) Value { + switch { + case sign < 0: + return NegInf + case sign > 0: + return PosInf + default: + return Zero + } +} + +// String returns a string representation of the Value +func (dn Value) String() string { + if dn.sign == 0 { + return "0" + } + const maxLeadingZeros = 7 + sign := "" + if dn.sign < 0 { + sign = "-" + } + if dn.IsInf() { + return sign + "inf" + } + digits := getDigits(dn.coef) + nd := len(digits) + e := int(dn.exp) - nd + if -maxLeadingZeros <= dn.exp && dn.exp <= 0 { + // decimal to the left + return sign + "." + strings.Repeat("0", -e-nd) + digits + } else if -nd < e && e <= -1 { + // decimal within + dec := nd + e + return sign + digits[:dec] + "." + digits[dec:] + } else if 0 < dn.exp && dn.exp <= digitsMax { + // decimal to the right + return sign + digits + strings.Repeat("0", e) + } else { + // scientific notation + after := "" + if nd > 1 { + after = "." + digits[1:] + } + return sign + digits[:1] + after + "e" + strconv.Itoa(int(dn.exp-1)) + } +} + +func (dn Value) Percentage() string { + if dn.sign == 0 { + return "0%" + } + const maxLeadingZeros = 7 + sign := "" + if dn.sign < 0 { + sign = "-" + } + if dn.IsInf() { + return sign + "inf%" + } + digits := getDigits(dn.coef) + nd := len(digits) + e := int(dn.exp) - nd + 2 + if -maxLeadingZeros <= dn.exp && dn.exp <= 0 { + // decimal to the left + return sign + "." + strings.Repeat("0", -e-nd) + digits + "%" + } else if -nd < e && e <= -1 { + // decimal within + dec := nd + e + return sign + digits[:dec] + "." + digits[dec:] + } else if 0 < dn.exp && dn.exp <= digitsMax { + // decimal to the right + return sign + digits + strings.Repeat("0", e) + "%" + } else { + // scientific notation + after := "" + if nd > 1 { + after = "." + digits[1:] + } + return sign + digits[:1] + after + "e" + strconv.Itoa(int(dn.exp-1)) + "%" + } +} + +func NumFractionalDigits(a Value) int { + i := shiftMax + coef := a.coef + nd := 0 + for coef != 0 && coef < pow10[i] { + i-- + } + for coef != 0 { + coef %= pow10[i] + i-- + nd++ + } + return nd - int(a.exp) +} + +func getDigits(coef uint64) string { + var digits [digitsMax]byte + i := shiftMax + nd := 0 + for coef != 0 { + digits[nd] = byte('0' + (coef / pow10[i])) + coef %= pow10[i] + nd++ + i-- + } + return string(digits[:nd]) +} + +func (v *Value) Scan(src interface {}) error { + var err error + switch d := src.(type) { + case int64: + *v = NewFromInt(d) + return nil + case float64: + *v = NewFromFloat(d) + return nil + case []byte: + *v, err = NewFromString(string(d)) + if err != nil { + return err + } + return nil + default: + } + return fmt.Errorf("fixedpoint.Value scan error, type %T is not supported, value: %+v", src, src) +} + +// NewFromString parses a numeric string and returns a Value representation. +func NewFromString(s string) (Value, error) { + length := len(s) + isPercentage := s[length - 1] == '%' + if isPercentage { + s = s[:length-1] + } + r := &reader{s, 0} + sign := getSign(r) + if r.matchStr("inf") { + return Inf(sign), nil + } + coef, exp := getCoef(r) + exp += getExp(r) + if r.len() != 0 { // didn't consume entire string + return Zero, errors.New("invalid number") + } else if coef == 0 || exp < math.MinInt8 { + return Zero, nil + } else if exp > math.MaxInt8 { + return Inf(sign), nil + } + if isPercentage { + exp -= 2 + } + //check(coefMin <= coef && coef <= coefMax) + return Value{coef, sign, exp}, nil +} + +func MustNewFromString(input string) Value { + v, err := NewFromString(input) + if err != nil { + panic(fmt.Errorf("cannot parse %s into fixedpoint, error: %s", input, err.Error())) + } + return v +} + +type reader struct { + s string + i int +} + +func (r *reader) cur() byte { + if r.i >= len(r.s) { + return 0 + } + return byte(r.s[r.i]) +} + +func (r *reader) prev() byte { + if r.i == 0 { + return 0 + } + return byte(r.s[r.i-1]) +} + +func (r *reader) len() int { + return len(r.s) - r.i +} + +func (r *reader) match(c byte) bool { + if r.cur() == c { + r.i++ + return true + } + return false +} + +func (r *reader) matchDigit() bool { + c := r.cur() + if '0' <= c && c <= '9' { + r.i++ + return true + } + return false +} + +func (r *reader) matchStr(pre string) bool { + if strings.HasPrefix(r.s[r.i:], pre) { + r.i += len(pre) + return true + } + return false +} + +func getSign(r *reader) int8 { + if r.match('-') { + return int8(signNeg) + } + r.match('+') + return int8(signPos) +} + +func getCoef(r *reader) (uint64, int) { + digits := false + beforeDecimal := true + for r.match('0') { + digits = true + } + if r.cur() == '.' && r.len() > 1 { + digits = false + } + n := uint64(0) + exp := 0 + p := shiftMax + for { + c := r.cur() + if r.matchDigit() { + digits = true + // ignore extra decimal places + if c != '0' && p >= 0 { + n += uint64(c-'0') * pow10[p] + } + p-- + } else if beforeDecimal { + // decimal point or end + exp = shiftMax - p + if !r.match('.') { + break + } + beforeDecimal = false + if !digits { + for r.match('0') { + digits = true + exp-- + } + } + } else { + break + } + } + if !digits { + panic("numbers require at least one digit") + } + return n, exp +} + +func getExp(r *reader) int { + e := 0 + if r.match('e') || r.match('E') { + esign := getSign(r) + for r.matchDigit() { + e = e*10 + int(r.prev()-'0') + } + e *= int(esign) + } + return e +} + +// end of FromStr --------------------------------------------------- + +// IsInf returns true if a Value is positive or negative infinite +func (dn Value) IsInf() bool { + return dn.sign == signPosInf || dn.sign == signNegInf +} + +// IsZero returns true if a Value is zero +func (dn Value) IsZero() bool { + return dn.sign == signZero +} + +// ToFloat converts a Value to float64 +func (dn Value) Float64() float64 { + if dn.IsInf() { + return math.Inf(int(dn.sign)) + } + g := float64(dn.coef) + if dn.sign == signNeg { + g = -g + } + e := pow10f[int(dn.exp) - digitsMax] + return g * e +} + +// Int64 converts a Value to an int64, returning whether it was convertible +func (dn Value) Int64() int64 { + if dn.sign == 0 { + return 0 + } + if dn.sign != signNegInf && dn.sign != signPosInf { + if 0 < dn.exp && dn.exp < digitsMax && + (dn.coef%pow10[digitsMax-dn.exp]) == 0 { // usual case + return int64(dn.sign) * int64(dn.coef/pow10[digitsMax-dn.exp]) + } + if dn.exp == digitsMax { + return int64(dn.sign) * int64(dn.coef) + } + if dn.exp == digitsMax+1 { + return int64(dn.sign) * (int64(dn.coef) * 10) + } + if dn.exp == digitsMax+2 { + return int64(dn.sign) * (int64(dn.coef) * 100) + } + if dn.exp == digitsMax+3 && dn.coef < math.MaxInt64/1000 { + return int64(dn.sign) * (int64(dn.coef) * 1000) + } + } + panic("unable to convert Value to int64") +} + +func (dn Value) Int() int { + // if int is int64, this is a nop + n := dn.Int64() + if int64(int(n)) != n { + panic("unable to convert Value to int32") + } + return int(n) +} + +// Sign returns -1 for negative, 0 for zero, and +1 for positive +func (dn Value) Sign() int { + return int(dn.sign) +} + +// Coef returns the coefficient +func (dn Value) Coef() uint64 { + return dn.coef +} + +// Exp returns the exponent +func (dn Value) Exp() int { + return int(dn.exp) +} + +// Frac returns the fractional portion, i.e. x - x.Int() +func (dn Value) Frac() Value { + if dn.sign == 0 || dn.sign == signNegInf || dn.sign == signPosInf || + dn.exp >= digitsMax { + return Zero + } + if dn.exp <= 0 { + return dn + } + frac := dn.coef % pow10[digitsMax-dn.exp] + if frac == dn.coef { + return dn + } + return New(dn.sign, frac, int(dn.exp)) +} + +type RoundingMode int + +const ( + Up RoundingMode = iota + Down + HalfUp +) + +// Trunc returns the integer portion (truncating any fractional part) +func (dn Value) Trunc() Value { + return dn.integer(Down) +} + +func (dn Value) integer(mode RoundingMode) Value { + if dn.sign == 0 || dn.sign == signNegInf || dn.sign == signPosInf || + dn.exp >= digitsMax { + return dn + } + if dn.exp <= 0 { + if mode == Up || + (mode == HalfUp && dn.exp == 0 && dn.coef >= One.coef*5) { + return New(dn.sign, One.coef, int(dn.exp)+1) + } + return Zero + } + e := digitsMax - dn.exp + frac := dn.coef % pow10[e] + if frac == 0 { + return dn + } + i := dn.coef - frac + if (mode == Up && frac > 0) || (mode == HalfUp && frac >= halfpow10[e]) { + return New(dn.sign, i+pow10[e], int(dn.exp)) // normalize + } + return Value{i, dn.sign, dn.exp} +} + +func (dn Value) Round(r int, mode RoundingMode) Value { + if dn.sign == 0 || dn.sign == signNegInf || dn.sign == signPosInf || + r >= digitsMax { + return dn + } + if r <= -digitsMax { + return Zero + } + n := New(dn.sign, dn.coef, int(dn.exp)+r) // multiply by 10^r + n = n.integer(mode) + if n.sign == signPos || n.sign == signNeg { // i.e. not zero or inf + return New(n.sign, n.coef, int(n.exp)-r) + } + return n +} + +// arithmetic operations ------------------------------------------------------- + +// Neg returns the Value negated i.e. sign reversed +func (dn Value) Neg() Value { + return Value{dn.coef, -dn.sign, dn.exp} +} + +// Abs returns the Value with a positive sign +func (dn Value) Abs() Value { + if dn.sign < 0 { + return Value{dn.coef, -dn.sign, dn.exp} + } + return dn +} + +// Equal returns true if two Value's are equal +func Equal(x, y Value) bool { + return x.sign == y.sign && x.exp == y.exp && x.coef == y.coef +} + +func (x Value) Eq(y Value) bool { + return Equal(x, y) +} + +func Max(x, y Value) Value { + if Compare(x, y) > 0 { + return x + } + return y +} + +func Min(x, y Value) Value { + if Compare(x, y) < 0 { + return x + } + return y +} + +// Compare compares two Value's returning -1 for <, 0 for ==, +1 for > +func Compare(x, y Value) int { + switch { + case x.sign < y.sign: + return -1 + case x.sign > y.sign: + return 1 + case x == y: + return 0 + } + sign := int(x.sign) + switch { + case sign == 0 || sign == signNegInf || sign == signPosInf: + return 0 + case x.exp < y.exp: + return -sign + case x.exp > y.exp: + return +sign + case x.coef < y.coef: + return -sign + case x.coef > y.coef: + return +sign + default: + return 0 + } +} + +func (x Value) Compare(y Value) int { + return Compare(x, y) +} + +func (v *Value) UnmarshalYAML(unmarshal func(a interface{}) error) (err error) { + var f float64 + if err = unmarshal(&f); err == nil { + *v = NewFromFloat(f) + return + } + var i int64 + if err = unmarshal(&i); err == nil { + *v = NewFromInt(i) + return + } + + var s string + if err = unmarshal(&s); err == nil { + nv, err2 := NewFromString(s) + if err2 == nil { + *v = nv + return + } + } + return err +} + +func (v Value) MarshalJSON() ([]byte, error) { + return []byte(v.String()), nil +} + +func (v *Value) UnmarshalJSON(data []byte) error { + var a interface{} + err := json.Unmarshal(data, &a) + if err != nil { + return err + } + switch d := a.(type) { + case float64: + *v = NewFromFloat(d) + case float32: + *v = NewFromFloat(float64(d)) + case int: + *v = NewFromInt(int64(d)) + case int64: + *v = NewFromInt(d) + case string: + v2, err := NewFromString(d) + if err != nil { + return err + } + *v = v2; + default: + return fmt.Errorf("unsupported type :%T %v", d, d) + } + return nil +} + +func Must(v Value, err error) Value { + if err != nil { + panic(err) + } + return v +} + +// Sub returns the difference of two Value's +func Sub(x, y Value) Value { + return Add(x, y.Neg()) +} + +func (x Value) Sub(y Value) Value { + return Sub(x, y) +} + +// Add returns the sum of two Value's +func Add(x, y Value) Value { + switch { + case x.sign == signZero: + return y + case y.sign == signZero: + return x + case x.IsInf(): + if y.sign == -x.sign { + return Zero + } + return x + case y.IsInf(): + return y + } + if !align(&x, &y) { + return x + } + if x.sign != y.sign { + return usub(x, y) + } + return uadd(x, y) +} + +func (x Value) Add(y Value) Value { + return Add(x, y) +} + +func uadd(x, y Value) Value { + return New(x.sign, x.coef+y.coef, int(x.exp)) +} + +func usub(x, y Value) Value { + if x.coef < y.coef { + return New(-x.sign, y.coef-x.coef, int(x.exp)) + } + return New(x.sign, x.coef-y.coef, int(x.exp)) +} + +func align(x, y *Value) bool { + if x.exp == y.exp { + return true + } + if x.exp < y.exp { + *x, *y = *y, *x // swap + } + yshift := ilog10(y.coef) + e := int(x.exp - y.exp) + if e > yshift { + return false + } + yshift = e + //check(0 <= yshift && yshift <= 20) + y.coef = (y.coef + halfpow10[yshift]) / pow10[yshift] + //check(int(y.exp)+yshift == int(x.exp)) + return true +} + +const e7 = 10000000 + +// Mul returns the product of two Value's +func Mul(x, y Value) Value { + sign := x.sign * y.sign + switch { + case sign == signZero: + return Zero + case x.IsInf() || y.IsInf(): + return Inf(sign) + } + e := int(x.exp) + int(y.exp) + + // split unevenly to use full 64 bit range to get more precision + // and avoid needing xlo * ylo + xhi := x.coef / e7 // 9 digits + xlo := x.coef % e7 // 7 digits + yhi := y.coef / e7 // 9 digits + ylo := y.coef % e7 // 7 digits + + c := xhi * yhi + if xlo != 0 || ylo != 0 { + c += (xlo*yhi + ylo*xhi) / e7 + } + return New(sign, c, e-2) +} + +func (x Value) Mul(y Value) Value { + return Mul(x, y) +} + +// Div returns the quotient of two Value's +func Div(x, y Value) Value { + sign := x.sign * y.sign + switch { + case x.sign == signZero: + return x + case y.sign == signZero: + return Inf(x.sign) + case x.IsInf(): + if y.IsInf() { + if sign < 0 { + return NegOne + } + return One + } + return Inf(sign) + case y.IsInf(): + return Zero + } + coef := div128(x.coef, y.coef) + return New(sign, coef, int(x.exp)-int(y.exp)) +} + +func (x Value) Div(y Value) Value { + return Div(x, y) +} + +// Hash returns a hash value for a Value +func (dn Value) Hash() uint32 { + return uint32(dn.coef>>32) ^ uint32(dn.coef) ^ + uint32(dn.sign)<<16 ^ uint32(dn.exp)<<8 +} + +// Format converts a number to a string with a specified format +func (dn Value) Format(mask string) string { + if dn.IsInf() { + return "#" + } + n := dn + before := 0 + after := 0 + intpart := true + for _, mc := range mask { + switch mc { + case '.': + intpart = false + case '#': + if intpart { + before++ + } else { + after++ + } + } + } + if before+after == 0 || n.Exp() > before { + return "#" // too big to fit in mask + } + n = n.Round(after, HalfUp) + e := n.Exp() + var digits []byte + if n.IsZero() && after == 0 { + digits = []byte("0") + e = 1 + } else { + digits = strconv.AppendUint(make([]byte, 0, digitsMax), n.Coef(), 10) + digits = bytes.TrimRight(digits, "0") + } + nd := len(digits) + + di := e - before + //check(di <= 0) + var buf strings.Builder + sign := n.Sign() + signok := (sign >= 0) + frac := false + for _, mc := range []byte(mask) { + switch mc { + case '#': + if 0 <= di && di < nd { + buf.WriteByte(digits[di]) + } else if frac || di >= 0 { + buf.WriteByte('0') + } + di++ + case ',': + if di > 0 { + buf.WriteByte(',') + } + case '-', '(': + signok = true + if sign < 0 { + buf.WriteByte(mc) + } + case ')': + if sign < 0 { + buf.WriteByte(mc) + } else { + buf.WriteByte(' ') + } + case '.': + frac = true + fallthrough + default: + buf.WriteByte(mc) + } + } + if !signok { + return "-" // negative not handled by mask + } + return buf.String() +} diff --git a/pkg/fixedpoint/convert_test.go b/pkg/fixedpoint/dec_test.go similarity index 90% rename from pkg/fixedpoint/convert_test.go rename to pkg/fixedpoint/dec_test.go index b2135f8770..9b20d31818 100644 --- a/pkg/fixedpoint/convert_test.go +++ b/pkg/fixedpoint/dec_test.go @@ -2,7 +2,7 @@ package fixedpoint import ( "testing" - + "math/big" "github.com/stretchr/testify/assert" ) @@ -27,29 +27,31 @@ func BenchmarkMul(b *testing.B) { b.Run("mul-big-small-numbers", func(b *testing.B) { for i := 0; i < b.N; i++ { - x := NewFromFloat(20.0) - y := NewFromFloat(20.0) - x = x.BigMul(y) + x := big.NewFloat(20.0) + y := big.NewFloat(20.0) + x = new(big.Float).Mul(x, y) } }) b.Run("mul-big-large-numbers", func(b *testing.B) { for i := 0; i < b.N; i++ { - x := NewFromFloat(88.12345678) - y := NewFromFloat(88.12345678) - x = x.BigMul(y) + x := big.NewFloat(88.12345678) + y := big.NewFloat(88.12345678) + x = new(big.Float).Mul(x, y) } }) } -func TestBigMul(t *testing.T) { +func TestMulString(t *testing.T) { x := NewFromFloat(10.55) + assert.Equal(t, "10.55", x.String()) y := NewFromFloat(10.55) - x = x.BigMul(y) - assert.Equal(t, NewFromFloat(111.3025), x) + x = x.Mul(y) + assert.Equal(t, "111.3025", x.String()) } -func TestParse(t *testing.T) { +// Not used +/*func TestParse(t *testing.T) { type args struct { input string } @@ -118,7 +120,7 @@ func TestParse(t *testing.T) { } }) } -} +}*/ func TestNumFractionalDigits(t *testing.T) { tests := []struct { @@ -129,7 +131,7 @@ func TestNumFractionalDigits(t *testing.T) { { name: "over the default precision", v: MustNewFromString("0.123456789"), - want: 8, + want: 9, }, { name: "ignore the integer part", diff --git a/pkg/fixedpoint/div128.go b/pkg/fixedpoint/div128.go new file mode 100644 index 0000000000..8a7f8d288d --- /dev/null +++ b/pkg/fixedpoint/div128.go @@ -0,0 +1,132 @@ +// Copyright Suneido Software Corp. All rights reserved. +// Governed by the MIT license found in the LICENSE file. + +package fixedpoint + +import ( + "math/bits" +) + +const ( + e16 = 1_0000_0000_0000_0000 + longMask = 0xffffffff + divNumBase = 1 << 32 + e16Hi = e16 >> 32 + e16Lo = e16 & longMask +) + +// returns (1e16 * dividend) / divisor +// Used by dnum divide +// Based on cSuneido code +// which is based on jSuneido code +// which is based on Java BigDecimal code +// which is based on Hacker's Delight and Knuth TAoCP Vol 2 +// A bit simpler with unsigned types +func div128(dividend, divisor uint64) uint64 { + //check(dividend != 0) + //check(divisor != 0) + // multiply dividend * e16 + d1Hi := dividend >> 32 + d1Lo := dividend & longMask + product := uint64(e16Lo) * d1Lo + d0 := product & longMask + d1 := product >> 32 + product = uint64(e16Hi)*d1Lo + d1 + d1 = product & longMask + d2 := product >> 32 + product = uint64(e16Lo)*d1Hi + d1 + d1 = product & longMask + d2 += product >> 32 + d3 := d2 >> 32 + d2 &= longMask + product = e16Hi*d1Hi + d2 + d2 = product & longMask + d3 = ((product >> 32) + d3) & longMask + dividendHi := make64(uint32(d3), uint32(d2)) + dividendLo := make64(uint32(d1), uint32(d0)) + // divide + return divide128(dividendHi, dividendLo, divisor) +} + +func divide128(dividendHi, dividendLo, divisor uint64) uint64 { + // so we can shift dividend as much as divisor + // don't allow equals to avoid quotient overflow (by 1) + //check(dividendHi < divisor) + + // maximize divisor (bit wise), since we're mostly using the top half + shift := uint(bits.LeadingZeros64(divisor)) + divisor = divisor << shift + + // split divisor + v1 := divisor >> 32 + v0 := divisor & longMask + + // matching shift + dls := dividendLo << shift + // split dividendLo + u1 := uint32(dls >> 32) + u0 := uint32(dls & longMask) + + // tmp1 = top 64 of dividend << shift + tmp1 := (dividendHi << shift) | (dividendLo >> (64 - shift)) + var q1, rtmp1 uint64 + if v1 == 1 { + q1 = tmp1 + rtmp1 = 0 + } else { + //check(tmp1 >= 0) + q1 = tmp1 / v1 // DIVIDE top 64 / top 32 + rtmp1 = tmp1 % v1 // remainder + } + + // adjust if quotient estimate too large + //check(q1 < divNumBase) + for q1*v0 > make64(uint32(rtmp1), u1) { + // done about 5.5 per 10,000 divides + q1-- + rtmp1 += v1 + if rtmp1 >= divNumBase { + break + } + } + //check(q1 >= 0) + u2 := tmp1 & longMask // low half + + // u2,u1 is the MIDDLE 64 bits of the dividend + tmp2 := mulsub(uint32(u2), uint32(u1), uint32(v1), uint32(v0), q1) + var q0, rtmp2 uint64 + if v1 == 1 { + q0 = tmp2 + rtmp2 = 0 + } else { + q0 = tmp2 / v1 // DIVIDE dividend remainder 64 / divisor high 32 + rtmp2 = tmp2 % v1 + } + + // adjust if quotient estimate too large + //check(q0 < divNumBase) + for q0*v0 > make64(uint32(rtmp2), u0) { + // done about .33 times per divide + q0-- + rtmp2 += v1 + if rtmp2 >= divNumBase { + break + } + //check(q0 < divNumBase) + } + + //check(q1 <= math.MaxUint32) + //check(q0 <= math.MaxUint32) + return make64(uint32(q1), uint32(q0)) +} + +// mulsub returns u1,u0 - v1,v0 * q0 +func mulsub(u1, u0, v1, v0 uint32, q0 uint64) uint64 { + tmp := uint64(u0) - q0*uint64(v0) + return make64(u1+uint32(tmp>>32)-uint32(q0*uint64(v1)), uint32(tmp&longMask)) +} + +func make64(hi, lo uint32) uint64 { + return uint64(hi)<<32 | uint64(lo) +} + diff --git a/pkg/indicator/obv.go b/pkg/indicator/obv.go index a15dd93878..0e2956b7f9 100644 --- a/pkg/indicator/obv.go +++ b/pkg/indicator/obv.go @@ -4,6 +4,7 @@ import ( "time" "github.com/c9s/bbgo/pkg/types" + "github.com/c9s/bbgo/pkg/fixedpoint" ) /* @@ -16,10 +17,10 @@ On-Balance Volume (OBV) Definition type OBV struct { types.IntervalWindow Values types.Float64Slice - PrePrice float64 + PrePrice fixedpoint.Value EndTime time.Time - UpdateCallbacks []func(value float64) + UpdateCallbacks []func(value fixedpoint.Value) } func (inc *OBV) update(kLine types.KLine, priceF KLinePriceMapper) { diff --git a/pkg/strategy/grid/strategy.go b/pkg/strategy/grid/strategy.go index b595043686..7fe4e1adae 100644 --- a/pkg/strategy/grid/strategy.go +++ b/pkg/strategy/grid/strategy.go @@ -552,7 +552,7 @@ func (s *Strategy) SaveState() error { // 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, s.LowerPrice) + return fmt.Sprintf("%s-%s-%d-%d-%d", ID, s.Symbol, s.GridNum, s.UpperPrice.Int(), s.LowerPrice.Int()) } func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { diff --git a/pkg/types/account.go b/pkg/types/account.go index 29ddb762af..8dfe56120b 100644 --- a/pkg/types/account.go +++ b/pkg/types/account.go @@ -29,15 +29,15 @@ type Balance struct { } func (b Balance) Total() fixedpoint.Value { - return b.Available + b.Locked + return b.Available.Add(b.Locked) } func (b Balance) String() string { - if b.Locked > 0 { - return fmt.Sprintf("%s: %f (locked %f)", b.Currency, b.Available.Float64(), b.Locked.Float64()) + if b.Locked.Sign() > 0 { + return fmt.Sprintf("%s: %s (locked %s)", b.Currency, b.Available.String(), b.Locked.String()) } - return fmt.Sprintf("%s: %f", b.Currency, b.Available.Float64()) + return fmt.Sprintf("%s: %s", b.Currency, b.Available.String()) } type Asset struct { @@ -58,9 +58,9 @@ func (m AssetMap) PlainText() (o string) { for _, a := range m { usd := a.InUSD.Float64() btc := a.InBTC.Float64() - o += fmt.Sprintf(" %s: %f (≈ %s) (≈ %s)", + o += fmt.Sprintf(" %s: %s (≈ %s) (≈ %s)", a.Currency, - a.Total.Float64(), + a.Total.String(), USD.FormatMoneyFloat64(usd), BTC.FormatMoneyFloat64(btc), ) + "\n" @@ -89,19 +89,19 @@ func (m AssetMap) SlackAttachment() slack.Attachment { // sort assets sort.Slice(assets, func(i, j int) bool { - return assets[i].InUSD > assets[j].InUSD + return assets[i].InUSD.Compare(assets[j].InUSD) > 0 }) for _, a := range assets { - totalUSD += a.InUSD - totalBTC += a.InBTC + totalUSD = totalUSD.Add(a.InUSD) + totalBTC = totalBTC.Add(a.InBTC) } for _, a := range assets { fields = append(fields, slack.AttachmentField{ Title: a.Currency, - Value: fmt.Sprintf("%f (≈ %s) (≈ %s) (%.2f%%)", - a.Total.Float64(), + Value: fmt.Sprintf("%s (≈ %s) (≈ %s) (%.2f%%)", + a.Total.String(), USD.FormatMoneyFloat64(a.InUSD.Float64()), BTC.FormatMoneyFloat64(a.InBTC.Float64()), math.Round(a.InUSD.Div(totalUSD).Float64()*100.0), @@ -143,18 +143,18 @@ func (m BalanceMap) Copy() (d BalanceMap) { return d } -func (m BalanceMap) Assets(prices map[string]float64) AssetMap { +func (m BalanceMap) Assets(prices map[string]fixedpoint.Value) AssetMap { assets := make(AssetMap) now := time.Now() for currency, b := range m { - if b.Locked == 0 && b.Available == 0 { + if b.Locked.IsZero() && b.Available.IsZero() { continue } asset := Asset{ Currency: currency, - Total: b.Available + b.Locked, + Total: b.Available.Add(b.Locked), Time: now, Locked: b.Locked, Available: b.Available, @@ -168,13 +168,13 @@ func (m BalanceMap) Assets(prices map[string]float64) AssetMap { if val, ok := prices[market]; ok { if strings.HasPrefix(market, "USD") { - asset.InUSD = fixedpoint.NewFromFloat(asset.Total.Float64() / val) + asset.InUSD = asset.Total.Div(val) } else { - asset.InUSD = asset.Total.MulFloat64(val) + asset.InUSD = asset.Total.Mul(val) } if hasBtcPrice { - asset.InBTC = fixedpoint.NewFromFloat(asset.InUSD.Float64() / btcusdt) + asset.InBTC = asset.InUSD.Div(btcusdt) } } } @@ -187,14 +187,14 @@ func (m BalanceMap) Assets(prices map[string]float64) AssetMap { func (m BalanceMap) Print() { for _, balance := range m { - if balance.Available == 0 && balance.Locked == 0 { + if balance.Available.IsZero() && balance.Locked.IsZero() { continue } - if balance.Locked > 0 { - logrus.Infof(" %s: %f (locked %f)", balance.Currency, balance.Available.Float64(), balance.Locked.Float64()) + if balance.Locked.Sign() > 0 { + logrus.Infof(" %s: %s (locked %s)", balance.Currency, balance.Available.String(), balance.Locked.String()) } else { - logrus.Infof(" %s: %f", balance.Currency, balance.Available.Float64()) + logrus.Infof(" %s: %s", balance.Currency, balance.Available.String()) } } } @@ -291,7 +291,7 @@ func (a *Account) AddBalance(currency string, fund fixedpoint.Value) { balance, ok := a.balances[currency] if ok { - balance.Available += fund + balance.Available = balance.Available.Add(fund) a.balances[currency] = balance return } @@ -299,7 +299,7 @@ func (a *Account) AddBalance(currency string, fund fixedpoint.Value) { a.balances[currency] = Balance{ Currency: currency, Available: fund, - Locked: 0, + Locked: fixedpoint.Zero, } } @@ -308,13 +308,13 @@ func (a *Account) UseLockedBalance(currency string, fund fixedpoint.Value) error defer a.Unlock() balance, ok := a.balances[currency] - if ok && balance.Locked >= fund { - balance.Locked -= fund + if ok && balance.Locked.Compare(fund) >= 0 { + balance.Locked = balance.Locked.Sub(fund) a.balances[currency] = balance return nil } - return fmt.Errorf("trying to use more than locked: locked %f < want to use %f", balance.Locked.Float64(), fund.Float64()) + return fmt.Errorf("trying to use more than locked: locked %s < want to use %s", balance.Locked.String(), fund.String()) } func (a *Account) UnlockBalance(currency string, unlocked fixedpoint.Value) error { @@ -326,12 +326,12 @@ func (a *Account) UnlockBalance(currency string, unlocked fixedpoint.Value) erro return fmt.Errorf("trying to unlocked inexisted balance: %s", currency) } - if unlocked > balance.Locked { - return fmt.Errorf("trying to unlocked more than locked %s: locked %f < want to unlock %f", currency, balance.Locked.Float64(), unlocked.Float64()) + if unlocked.Compare(balance.Locked) > 0 { + return fmt.Errorf("trying to unlocked more than locked %s: locked %s < want to unlock %s", currency, balance.Locked.String(), unlocked.String()) } - balance.Locked -= unlocked - balance.Available += unlocked + balance.Locked = balance.Locked.Sub(unlocked) + balance.Available = balance.Available.Add(unlocked) a.balances[currency] = balance return nil } @@ -341,14 +341,14 @@ func (a *Account) LockBalance(currency string, locked fixedpoint.Value) error { defer a.Unlock() balance, ok := a.balances[currency] - if ok && balance.Available >= locked { - balance.Locked += locked - balance.Available -= locked + if ok && balance.Available.Compare(locked) >= 0 { + balance.Locked = balance.Locked.Add(locked) + balance.Available = balance.Locked.Sub(locked) a.balances[currency] = balance return nil } - return fmt.Errorf("insufficient available balance %s for lock: want to lock %f, available %f", currency, locked.Float64(), balance.Available.Float64()) + return fmt.Errorf("insufficient available balance %s for lock: want to lock %s, available %s", currency, locked.String(), balance.Available.String()) } func (a *Account) UpdateBalances(balances BalanceMap) { @@ -384,11 +384,11 @@ func (a *Account) Print() { logrus.Infof("account type: %s", a.AccountType) } - if a.MakerFeeRate > 0 { - logrus.Infof("maker fee rate: %f", a.MakerFeeRate.Float64()) + if a.MakerFeeRate.Sign() > 0 { + logrus.Infof("maker fee rate: %s", a.MakerFeeRate.String()) } - if a.TakerFeeRate > 0 { - logrus.Infof("taker fee rate: %f", a.TakerFeeRate.Float64()) + if a.TakerFeeRate.Sign() > 0 { + logrus.Infof("taker fee rate: %s", a.TakerFeeRate.String()) } a.balances.Print() diff --git a/pkg/types/kline.go b/pkg/types/kline.go index 15c0200144..5a58cda224 100644 --- a/pkg/types/kline.go +++ b/pkg/types/kline.go @@ -2,12 +2,12 @@ package types import ( "fmt" - "math" "time" "github.com/slack-go/slack" "github.com/c9s/bbgo/pkg/util" + "github.com/c9s/bbgo/pkg/fixedpoint" ) type Direction int @@ -16,23 +16,25 @@ const DirectionUp = 1 const DirectionNone = 0 const DirectionDown = -1 +var Two = fixedpoint.NewFromInt(2) + type KLineOrWindow interface { GetInterval() string Direction() Direction - GetChange() float64 - GetMaxChange() float64 - GetThickness() float64 + GetChange() fixedpoint.Value + GetMaxChange() fixedpoint.Value + GetThickness() fixedpoint.Value - Mid() float64 - GetOpen() float64 - GetClose() float64 - GetHigh() float64 - GetLow() float64 + Mid() fixedpoint.Value + GetOpen() fixedpoint.Value + GetClose() fixedpoint.Value + GetHigh() fixedpoint.Value + GetLow() fixedpoint.Value BounceUp() bool BounceDown() bool - GetUpperShadowRatio() float64 - GetLowerShadowRatio() float64 + GetUpperShadowRatio() fixedpoint.Value + GetLowerShadowRatio() fixedpoint.Value SlackAttachment() slack.Attachment } @@ -55,14 +57,14 @@ type KLine struct { Interval Interval `json:"interval" db:"interval"` - Open float64 `json:"open" db:"open"` - Close float64 `json:"close" db:"close"` - High float64 `json:"high" db:"high"` - Low float64 `json:"low" db:"low"` - Volume float64 `json:"volume" db:"volume"` - QuoteVolume float64 `json:"quoteVolume" db:"quote_volume"` - TakerBuyBaseAssetVolume float64 `json:"takerBuyBaseAssetVolume" db:"taker_buy_base_volume"` - TakerBuyQuoteAssetVolume float64 `json:"takerBuyQuoteAssetVolume" db:"taker_buy_quote_volume"` + Open fixedpoint.Value `json:"open" db:"open"` + Close fixedpoint.Value `json:"close" db:"close"` + High fixedpoint.Value `json:"high" db:"high"` + Low fixedpoint.Value `json:"low" db:"low"` + Volume fixedpoint.Value `json:"volume" db:"volume"` + QuoteVolume fixedpoint.Value `json:"quoteVolume" db:"quote_volume"` + TakerBuyBaseAssetVolume fixedpoint.Value `json:"takerBuyBaseAssetVolume" db:"taker_buy_base_volume"` + TakerBuyQuoteAssetVolume fixedpoint.Value `json:"takerBuyQuoteAssetVolume" db:"taker_buy_quote_volume"` LastTradeID uint64 `json:"lastTradeID" db:"last_trade_id"` NumberOfTrades uint64 `json:"numberOfTrades" db:"num_trades"` @@ -81,100 +83,114 @@ func (k KLine) GetInterval() Interval { return k.Interval } -func (k KLine) Mid() float64 { - return (k.High + k.Low) / 2 +func (k KLine) Mid() fixedpoint.Value { + return k.High.Add(k.Low).Div(Two) } // green candle with open and close near high price func (k KLine) BounceUp() bool { mid := k.Mid() trend := k.Direction() - return trend > 0 && k.Open > mid && k.Close > mid + return trend > 0 && k.Open.Compare(mid) > 0 && k.Close.Compare(mid) > 0 } // red candle with open and close near low price func (k KLine) BounceDown() bool { mid := k.Mid() trend := k.Direction() - return trend > 0 && k.Open < mid && k.Close < mid + return trend > 0 && k.Open.Compare(mid) < 0 && k.Close.Compare(mid) < 0 } func (k KLine) Direction() Direction { o := k.GetOpen() c := k.GetClose() - if c > o { + if c.Compare(o) > 0 { return DirectionUp - } else if c < o { + } else if c.Compare(o) < 0 { return DirectionDown } return DirectionNone } -func (k KLine) GetHigh() float64 { +func (k KLine) GetHigh() fixedpoint.Value { return k.High } -func (k KLine) GetLow() float64 { +func (k KLine) GetLow() fixedpoint.Value { return k.Low } -func (k KLine) GetOpen() float64 { +func (k KLine) GetOpen() fixedpoint.Value { return k.Open } -func (k KLine) GetClose() float64 { +func (k KLine) GetClose() fixedpoint.Value { return k.Close } -func (k KLine) GetMaxChange() float64 { - return k.GetHigh() - k.GetLow() +func (k KLine) GetMaxChange() fixedpoint.Value { + return k.GetHigh().Sub(k.GetLow()) } -func (k KLine) GetAmplification() float64 { - return k.GetMaxChange() / k.GetLow() +func (k KLine) GetAmplification() fixedpoint.Value { + return k.GetMaxChange().Div(k.GetLow()) } // GetThickness returns the thickness of the kline. 1 => thick, 0.1 => thin -func (k KLine) GetThickness() float64 { - return math.Abs(k.GetChange()) / math.Abs(k.GetMaxChange()) +func (k KLine) GetThickness() fixedpoint.Value { + out := k.GetChange().Div(k.GetMaxChange()) + if out.Sign() < 0 { + return out.Neg() + } + return out } -func (k KLine) GetUpperShadowRatio() float64 { - return k.GetUpperShadowHeight() / math.Abs(k.GetMaxChange()) +func (k KLine) GetUpperShadowRatio() fixedpoint.Value { + out := k.GetUpperShadowHeight().Div(k.GetMaxChange()) + if out.Sign() < 0 { + return out.Neg() + } + return out } -func (k KLine) GetUpperShadowHeight() float64 { +func (k KLine) GetUpperShadowHeight() fixedpoint.Value { high := k.GetHigh() - if k.GetOpen() > k.GetClose() { - return high - k.GetOpen() + open := k.GetOpen() + clos := k.GetClose() + if open.Compare(clos) > 0 { + return high.Sub(open) } - return high - k.GetClose() + return high.Sub(clos) } -func (k KLine) GetLowerShadowRatio() float64 { - return k.GetLowerShadowHeight() / math.Abs(k.GetMaxChange()) +func (k KLine) GetLowerShadowRatio() fixedpoint.Value { + out := k.GetLowerShadowHeight().Div(k.GetMaxChange()) + if out.Sign() < 0 { + return out.Neg() + } + return out } -func (k KLine) GetLowerShadowHeight() float64 { +func (k KLine) GetLowerShadowHeight() fixedpoint.Value { low := k.Low - if k.Open < k.Close { // uptrend - return k.Open - low + if k.Open.Compare(k.Close) < 0 { // uptrend + return k.Open.Sub(low) } // downtrend - return k.Close - low + return k.Close.Sub(low) } // GetBody returns the height of the candle real body -func (k KLine) GetBody() float64 { +func (k KLine) GetBody() fixedpoint.Value { return k.GetChange() } // GetChange returns Close price - Open price. -func (k KLine) GetChange() float64 { - return k.Close - k.Open +func (k KLine) GetChange() fixedpoint.Value { + return k.Close.Sub(k.Open) } func (k KLine) Color() string { @@ -190,7 +206,7 @@ func (k KLine) String() string { return fmt.Sprintf("%s %s %s %s O: %.4f H: %.4f L: %.4f C: %.4f CHG: %.4f MAXCHG: %.4f V: %.4f QV: %.2f TBBV: %.2f", k.Exchange.String(), k.StartTime.Time().Format("2006-01-02 15:04"), - k.Symbol, k.Interval, k.Open, k.High, k.Low, k.Close, k.GetChange(), k.GetMaxChange(), k.Volume, k.QuoteVolume, k.TakerBuyBaseAssetVolume) + k.Symbol, k.Interval, k.Open.Float64(), k.High.Float64(), k.Low.Float64(), k.Close.Float64(), k.GetChange().Float64(), k.GetMaxChange().Float64(), k.Volume.Float64(), k.QuoteVolume.Float64(), k.TakerBuyBaseAssetVolume.Float64()) } func (k KLine) PlainText() string { @@ -202,29 +218,29 @@ func (k KLine) SlackAttachment() slack.Attachment { Text: fmt.Sprintf("*%s* KLine %s", k.Symbol, k.Interval), Color: k.Color(), Fields: []slack.AttachmentField{ - {Title: "Open", Value: util.FormatFloat(k.Open, 2), Short: true}, - {Title: "High", Value: util.FormatFloat(k.High, 2), Short: true}, - {Title: "Low", Value: util.FormatFloat(k.Low, 2), Short: true}, - {Title: "Close", Value: util.FormatFloat(k.Close, 2), Short: true}, - {Title: "Mid", Value: util.FormatFloat(k.Mid(), 2), Short: true}, - {Title: "Change", Value: util.FormatFloat(k.GetChange(), 2), Short: true}, - {Title: "Volume", Value: util.FormatFloat(k.Volume, 2), Short: true}, - {Title: "Taker Buy Base Volume", Value: util.FormatFloat(k.TakerBuyBaseAssetVolume, 2), Short: true}, - {Title: "Taker Buy Quote Volume", Value: util.FormatFloat(k.TakerBuyQuoteAssetVolume, 2), Short: true}, - {Title: "Max Change", Value: util.FormatFloat(k.GetMaxChange(), 2), Short: true}, + {Title: "Open", Value: util.FormatValue(k.Open, 2), Short: true}, + {Title: "High", Value: util.FormatValue(k.High, 2), Short: true}, + {Title: "Low", Value: util.FormatValue(k.Low, 2), Short: true}, + {Title: "Close", Value: util.FormatValue(k.Close, 2), Short: true}, + {Title: "Mid", Value: util.FormatValue(k.Mid(), 2), Short: true}, + {Title: "Change", Value: util.FormatValue(k.GetChange(), 2), Short: true}, + {Title: "Volume", Value: util.FormatValue(k.Volume, 2), Short: true}, + {Title: "Taker Buy Base Volume", Value: util.FormatValue(k.TakerBuyBaseAssetVolume, 2), Short: true}, + {Title: "Taker Buy Quote Volume", Value: util.FormatValue(k.TakerBuyQuoteAssetVolume, 2), Short: true}, + {Title: "Max Change", Value: util.FormatValue(k.GetMaxChange(), 2), Short: true}, { Title: "Thickness", - Value: util.FormatFloat(k.GetThickness(), 4), + Value: util.FormatValue(k.GetThickness(), 4), Short: true, }, { Title: "UpperShadowRatio", - Value: util.FormatFloat(k.GetUpperShadowRatio(), 4), + Value: util.FormatValue(k.GetUpperShadowRatio(), 4), Short: true, }, { Title: "LowerShadowRatio", - Value: util.FormatFloat(k.GetLowerShadowRatio(), 4), + Value: util.FormatValue(k.GetLowerShadowRatio(), 4), Short: true, }, }, @@ -236,11 +252,11 @@ func (k KLine) SlackAttachment() slack.Attachment { type KLineWindow []KLine // ReduceClose reduces the closed prices -func (k KLineWindow) ReduceClose() float64 { - s := 0.0 +func (k KLineWindow) ReduceClose() fixedpoint.Value { + s := fixedpoint.Zero for _, kline := range k { - s += kline.GetClose() + s = s.Add(kline.GetClose()) } return s @@ -262,43 +278,43 @@ func (k KLineWindow) GetInterval() Interval { return k.First().Interval } -func (k KLineWindow) GetOpen() float64 { +func (k KLineWindow) GetOpen() fixedpoint.Value { return k.First().GetOpen() } -func (k KLineWindow) GetClose() float64 { +func (k KLineWindow) GetClose() fixedpoint.Value { end := len(k) - 1 return k[end].GetClose() } -func (k KLineWindow) GetHigh() float64 { +func (k KLineWindow) GetHigh() fixedpoint.Value { high := k.First().GetHigh() for _, line := range k { - high = math.Max(high, line.GetHigh()) + high = fixedpoint.Max(high, line.GetHigh()) } return high } -func (k KLineWindow) GetLow() float64 { +func (k KLineWindow) GetLow() fixedpoint.Value { low := k.First().GetLow() for _, line := range k { - low = math.Min(low, line.GetLow()) + low = fixedpoint.Min(low, line.GetLow()) } return low } -func (k KLineWindow) GetChange() float64 { - return k.GetClose() - k.GetOpen() +func (k KLineWindow) GetChange() fixedpoint.Value { + return k.GetClose().Sub(k.GetOpen()) } -func (k KLineWindow) GetMaxChange() float64 { - return k.GetHigh() - k.GetLow() +func (k KLineWindow) GetMaxChange() fixedpoint.Value { + return k.GetHigh().Sub(k.GetLow()) } -func (k KLineWindow) GetAmplification() float64 { - return k.GetMaxChange() / k.GetLow() +func (k KLineWindow) GetAmplification() fixedpoint.Value { + return k.GetMaxChange().Div(k.GetLow()) } func (k KLineWindow) AllDrop() bool { @@ -323,9 +339,9 @@ func (k KLineWindow) GetTrend() int { o := k.GetOpen() c := k.GetClose() - if c > o { + if c.Compare(o) > 0 { return 1 - } else if c < o { + } else if c.Compare(o) < 0 { return -1 } return 0 @@ -341,22 +357,22 @@ func (k KLineWindow) Color() string { } // Mid price -func (k KLineWindow) Mid() float64 { - return (k.GetHigh() + k.GetLow()) / 2.0 +func (k KLineWindow) Mid() fixedpoint.Value { + return k.GetHigh().Add(k.GetLow()).Div(Two) } // BounceUp returns true if it's green candle with open and close near high price func (k KLineWindow) BounceUp() bool { mid := k.Mid() trend := k.GetTrend() - return trend > 0 && k.GetOpen() > mid && k.GetClose() > mid + return trend > 0 && k.GetOpen().Compare(mid) > 0 && k.GetClose().Compare(mid) > 0 } // BounceDown returns true red candle with open and close near low price func (k KLineWindow) BounceDown() bool { mid := k.Mid() trend := k.GetTrend() - return trend > 0 && k.GetOpen() < mid && k.GetClose() < mid + return trend > 0 && k.GetOpen().Compare(mid) < 0 && k.GetClose().Compare(mid) < 0 } func (k *KLineWindow) Add(line KLine) { @@ -395,36 +411,52 @@ func (k *KLineWindow) Truncate(size int) { *k = kn } -func (k KLineWindow) GetBody() float64 { +func (k KLineWindow) GetBody() fixedpoint.Value { return k.GetChange() } -func (k KLineWindow) GetThickness() float64 { - return math.Abs(k.GetChange()) / math.Abs(k.GetMaxChange()) +func (k KLineWindow) GetThickness() fixedpoint.Value { + out := k.GetChange().Div(k.GetMaxChange()) + if out.Sign() < 0 { + return out.Neg() + } + return out } -func (k KLineWindow) GetUpperShadowRatio() float64 { - return k.GetUpperShadowHeight() / math.Abs(k.GetMaxChange()) +func (k KLineWindow) GetUpperShadowRatio() fixedpoint.Value { + out := k.GetUpperShadowHeight().Div(k.GetMaxChange()) + if out.Sign() < 0 { + return out.Neg() + } + return out } -func (k KLineWindow) GetUpperShadowHeight() float64 { +func (k KLineWindow) GetUpperShadowHeight() fixedpoint.Value { high := k.GetHigh() - if k.GetOpen() > k.GetClose() { - return high - k.GetOpen() + open := k.GetOpen() + clos := k.GetClose() + if open.Compare(clos) > 0 { + return high.Sub(open) } - return high - k.GetClose() + return high.Sub(clos) } -func (k KLineWindow) GetLowerShadowRatio() float64 { - return k.GetLowerShadowHeight() / math.Abs(k.GetMaxChange()) +func (k KLineWindow) GetLowerShadowRatio() fixedpoint.Value { + out := k.GetLowerShadowHeight().Div(k.GetMaxChange()) + if out.Sign() < 0 { + return out.Neg() + } + return out } -func (k KLineWindow) GetLowerShadowHeight() float64 { +func (k KLineWindow) GetLowerShadowHeight() fixedpoint.Value { low := k.GetLow() - if k.GetOpen() < k.GetClose() { - return k.GetOpen() - low + open := k.GetOpen() + clos := k.GetClose() + if open.Compare(clos) < 0 { + return open.Sub(low) } - return k.GetClose() - low + return clos.Sub(low) } func (k KLineWindow) SlackAttachment() slack.Attachment { @@ -439,35 +471,35 @@ func (k KLineWindow) SlackAttachment() slack.Attachment { return slack.Attachment{ Text: fmt.Sprintf("*%s* KLineWindow %s x %d", first.Symbol, first.Interval, windowSize), Color: k.Color(), - Fields: []slack.AttachmentField{ - {Title: "Open", Value: util.FormatFloat(k.GetOpen(), 2), Short: true}, - {Title: "High", Value: util.FormatFloat(k.GetHigh(), 2), Short: true}, - {Title: "Low", Value: util.FormatFloat(k.GetLow(), 2), Short: true}, - {Title: "Close", Value: util.FormatFloat(k.GetClose(), 2), Short: true}, - {Title: "Mid", Value: util.FormatFloat(k.Mid(), 2), Short: true}, + Fields: []slack.AttachmentField { + {Title: "Open", Value: util.FormatValue(k.GetOpen(), 2), Short: true}, + {Title: "High", Value: util.FormatValue(k.GetHigh(), 2), Short: true}, + {Title: "Low", Value: util.FormatValue(k.GetLow(), 2), Short: true}, + {Title: "Close", Value: util.FormatValue(k.GetClose(), 2), Short: true}, + {Title: "Mid", Value: util.FormatValue(k.Mid(), 2), Short: true}, { Title: "Change", - Value: util.FormatFloat(k.GetChange(), 2), + Value: util.FormatValue(k.GetChange(), 2), Short: true, }, { Title: "Max Change", - Value: util.FormatFloat(k.GetMaxChange(), 2), + Value: util.FormatValue(k.GetMaxChange(), 2), Short: true, }, { Title: "Thickness", - Value: util.FormatFloat(k.GetThickness(), 4), + Value: util.FormatValue(k.GetThickness(), 4), Short: true, }, { Title: "UpperShadowRatio", - Value: util.FormatFloat(k.GetUpperShadowRatio(), 4), + Value: util.FormatValue(k.GetUpperShadowRatio(), 4), Short: true, }, { Title: "LowerShadowRatio", - Value: util.FormatFloat(k.GetLowerShadowRatio(), 4), + Value: util.FormatValue(k.GetLowerShadowRatio(), 4), Short: true, }, }, diff --git a/pkg/types/kline_test.go b/pkg/types/kline_test.go index 9cb0c5ac9a..a49b05fc4e 100644 --- a/pkg/types/kline_test.go +++ b/pkg/types/kline_test.go @@ -2,15 +2,23 @@ package types import ( "testing" - "github.com/stretchr/testify/assert" + "encoding/json" ) func TestKLineWindow_Tail(t *testing.T) { - var win = KLineWindow{ + var jsonWin = []byte(`[ + {"open": 11600.0, "close": 11600.0, "high": 11600.0, "low": 11600.0}, + {"open": 11700.0, "close": 11700.0, "high": 11700.0, "low": 11700.0} + ]`) + var win KLineWindow + err := json.Unmarshal(jsonWin, &win) + assert.NoError(t, err) + + /*{ {Open: 11600.0, Close: 11600.0, High: 11600.0, Low: 11600.0}, {Open: 11700.0, Close: 11700.0, High: 11700.0, Low: 11700.0}, - } + }*/ var win2 = win.Tail(1) assert.Len(t, win2, 1) @@ -26,22 +34,25 @@ func TestKLineWindow_Tail(t *testing.T) { } func TestKLineWindow_Truncate(t *testing.T) { - var win = KLineWindow{ - {Open: 11600.0, Close: 11600.0, High: 11600.0, Low: 11600.0}, - {Open: 11601.0, Close: 11600.0, High: 11600.0, Low: 11600.0}, - {Open: 11602.0, Close: 11600.0, High: 11600.0, Low: 11600.0}, - {Open: 11603.0, Close: 11600.0, High: 11600.0, Low: 11600.0}, - } + var jsonWin = []byte(`[ + {"open": 11600.0, "close": 11600.0, "high": 11600.0, "low": 11600.0}, + {"open": 11601.0, "close": 11600.0, "high": 11600.0, "low": 11600.0}, + {"open": 11602.0, "close": 11600.0, "high": 11600.0, "low": 11600.0}, + {"open": 11603.0, "close": 11600.0, "high": 11600.0, "low": 11600.0} + ]`) + var win KLineWindow + err := json.Unmarshal(jsonWin, &win) + assert.NoError(t, err) win.Truncate(5) assert.Len(t, win, 4) - assert.Equal(t, 11603.0, win.Last().Open) + assert.Equal(t, 11603.0, win.Last().Open.Float64()) win.Truncate(3) assert.Len(t, win, 3) - assert.Equal(t, 11603.0, win.Last().Open) + assert.Equal(t, 11603.0, win.Last().Open.Float64()) win.Truncate(1) assert.Len(t, win, 1) - assert.Equal(t, 11603.0, win.Last().Open) + assert.Equal(t, 11603.0, win.Last().Open.Float64()) } diff --git a/pkg/types/position.go b/pkg/types/position.go index 34decacc13..2313649aa5 100644 --- a/pkg/types/position.go +++ b/pkg/types/position.go @@ -62,11 +62,12 @@ func (p *Position) NewClosePositionOrder(percentage float64) *SubmitOrder { } side := SideTypeSell - if base == 0 { + sign := base.Sign() + if sign == 0 { return nil - } else if base < 0 { + } else if sign < 0 { side = SideTypeBuy - } else if base > 0 { + } else if sign > 0 { side = SideTypeSell } @@ -135,13 +136,13 @@ func (p *Position) addTradeFee(trade Trade) { if p.TotalFee == nil { p.TotalFee = make(map[string]fixedpoint.Value) } - p.TotalFee[trade.FeeCurrency] = p.TotalFee[trade.FeeCurrency] + fixedpoint.NewFromFloat(trade.Fee) + p.TotalFee[trade.FeeCurrency] = p.TotalFee[trade.FeeCurrency].Add(trade.Fee) } func (p *Position) Reset() { - p.Base = 0 - p.Quote = 0 - p.AverageCost = 0 + p.Base = fixedpoint.Zero + p.Quote = fixedpoint.Zero + p.AverageCost = fixedpoint.Zero } func (p *Position) SetFeeRate(exchangeFee ExchangeFee) { @@ -157,9 +158,9 @@ func (p *Position) SetExchangeFeeRate(ex ExchangeName, exchangeFee ExchangeFee) } func (p *Position) Type() PositionType { - if p.Base > 0 { + if p.Base.Sign() > 0 { return PositionLong - } else if p.Base < 0 { + } else if p.Base.Sign() < 0 { return PositionShort } return PositionClosed @@ -176,11 +177,12 @@ func (p *Position) SlackAttachment() slack.Attachment { var posType = p.Type() var color = "" - if p.Base == 0 { + sign := p.Base.Sign() + if sign == 0 { color = "#cccccc" - } else if p.Base > 0 { + } else if sign > 0 { color = "#228B22" - } else if p.Base < 0 { + } else if sign < 0 { color = "#DC143C" } @@ -194,7 +196,7 @@ func (p *Position) SlackAttachment() slack.Attachment { if p.TotalFee != nil { for feeCurrency, fee := range p.TotalFee { - if fee > 0 { + if fee.Sign() > 0 { fields = append(fields, slack.AttachmentField{ Title: fmt.Sprintf("Fee (%s)", feeCurrency), Value: trimTrailingZeroFloat(fee.Float64()), @@ -237,9 +239,9 @@ func (p *Position) PlainText() (msg string) { func (p *Position) String() string { return fmt.Sprintf("POSITION %s: average cost = %f, base = %f, quote = %f", p.Symbol, - p.AverageCost.Float64(), - p.Base.Float64(), - p.Quote.Float64(), + p.AverageCost.String(), + p.Base.String(), + p.Quote.String(), ) } @@ -255,45 +257,45 @@ func (p *Position) AddTrades(trades []Trade) (fixedpoint.Value, fixedpoint.Value var totalProfitAmount, totalNetProfit fixedpoint.Value for _, trade := range trades { if profit, netProfit, madeProfit := p.AddTrade(trade); madeProfit { - totalProfitAmount += profit - totalNetProfit += netProfit + totalProfitAmount = totalProfitAmount.Add(profit) + totalNetProfit = totalNetProfit.Add(netProfit) } } - return totalProfitAmount, totalNetProfit, totalProfitAmount != 0 + return totalProfitAmount, totalNetProfit, !totalProfitAmount.IsZero() } func (p *Position) AddTrade(td Trade) (profit fixedpoint.Value, netProfit fixedpoint.Value, madeProfit bool) { - price := fixedpoint.NewFromFloat(td.Price) - quantity := fixedpoint.NewFromFloat(td.Quantity) - quoteQuantity := fixedpoint.NewFromFloat(td.QuoteQuantity) - fee := fixedpoint.NewFromFloat(td.Fee) + price := td.Price + quantity := td.Quantity + quoteQuantity := td.QuoteQuantity + fee := td.Fee // calculated fee in quote (some exchange accounts may enable platform currency fee discount, like BNB) - var feeInQuote fixedpoint.Value = 0 + var feeInQuote fixedpoint.Value = fixedpoint.Zero switch td.FeeCurrency { case p.BaseCurrency: - quantity -= fee + quantity = quantity.Sub(fee) case p.QuoteCurrency: - quoteQuantity -= fee + quoteQuantity = quoteQuantity.Sub(fee) default: if p.ExchangeFeeRates != nil { if exchangeFee, ok := p.ExchangeFeeRates[td.Exchange]; ok { if td.IsMaker { - feeInQuote += exchangeFee.MakerFeeRate.Mul(quoteQuantity) + feeInQuote = feeInQuote.Add(exchangeFee.MakerFeeRate.Mul(quoteQuantity)) } else { - feeInQuote += exchangeFee.TakerFeeRate.Mul(quoteQuantity) + feeInQuote = feeInQuote.Add(exchangeFee.TakerFeeRate.Mul(quoteQuantity)) } } } else if p.FeeRate != nil { if td.IsMaker { - feeInQuote += p.FeeRate.MakerFeeRate.Mul(quoteQuantity) + feeInQuote = feeInQuote.Add(p.FeeRate.MakerFeeRate.Mul(quoteQuantity)) } else { - feeInQuote += p.FeeRate.TakerFeeRate.Mul(quoteQuantity) + feeInQuote = feeInQuote.Add(p.FeeRate.TakerFeeRate.Mul(quoteQuantity)) } } } @@ -308,61 +310,71 @@ func (p *Position) AddTrade(td Trade) (profit fixedpoint.Value, netProfit fixedp switch td.Side { case SideTypeBuy: - if p.Base < 0 { + if p.Base.Sign() < 0 { // convert short position to long position - if p.Base+quantity > 0 { - profit = (p.AverageCost - price).Mul(-p.Base) - netProfit = (p.ApproximateAverageCost - price).Mul(-p.Base) - feeInQuote - p.Base += quantity - p.Quote -= quoteQuantity + if p.Base.Add(quantity).Sign() > 0 { + profit = p.AverageCost.Sub(price).Mul(p.Base.Neg()) + netProfit = p.ApproximateAverageCost.Sub(price).Mul(p.Base.Neg()).Sub(feeInQuote) + p.Base = p.Base.Add(quantity) + p.Quote = p.Quote.Sub(quoteQuantity) p.AverageCost = price p.ApproximateAverageCost = price return profit, netProfit, true } else { // covering short position - p.Base += quantity - p.Quote -= quoteQuantity - profit = (p.AverageCost - price).Mul(quantity) - netProfit = (p.ApproximateAverageCost - price).Mul(quantity) - feeInQuote + p.Base = p.Base.Add(quantity) + p.Quote = p.Quote.Sub(quoteQuantity) + profit = p.AverageCost.Sub(price).Mul(quantity) + netProfit = p.ApproximateAverageCost.Sub(price).Mul(quantity).Sub(feeInQuote) return profit, netProfit, true } } - p.ApproximateAverageCost = (p.ApproximateAverageCost.Mul(p.Base) + quoteQuantity + feeInQuote).Div(p.Base + quantity) - p.AverageCost = (p.AverageCost.Mul(p.Base) + quoteQuantity).Div(p.Base + quantity) - p.Base += quantity - p.Quote -= quoteQuantity + dividor := p.Base.Add(quantity) + p.ApproximateAverageCost = p.ApproximateAverageCost.Mul(p.Base). + Add(quoteQuantity). + Add(feeInQuote). + Div(dividor) + p.AverageCost = p.AverageCost.Mul(p.Base).Add(quoteQuantity).Div(dividor) + p.Base = p.Base.Add(quantity) + p.Quote = p.Quote.Sub(quoteQuantity) - return 0, 0, false + return fixedpoint.Zero, fixedpoint.Zero, false case SideTypeSell: - if p.Base > 0 { + if p.Base.Sign() > 0 { // convert long position to short position - if p.Base-quantity < 0 { - profit = (price - p.AverageCost).Mul(p.Base) - netProfit = (price - p.ApproximateAverageCost).Mul(p.Base) - feeInQuote - p.Base -= quantity - p.Quote += quoteQuantity + if p.Base.Compare(quantity) < 0 { + profit = price.Sub(p.AverageCost).Mul(p.Base) + netProfit = price.Sub(p.ApproximateAverageCost).Mul(p.Base).Sub(feeInQuote) + p.Base = p.Base.Sub(quantity) + p.Quote = p.Quote.Add(quoteQuantity) p.AverageCost = price p.ApproximateAverageCost = price return profit, netProfit, true } else { - p.Base -= quantity - p.Quote += quoteQuantity - profit = (price - p.AverageCost).Mul(quantity) - netProfit = (price - p.ApproximateAverageCost).Mul(quantity) - feeInQuote + p.Base = p.Base.Sub(quantity) + p.Quote = p.Quote.Add(quoteQuantity) + profit = price.Sub(p.AverageCost).Mul(quantity) + netProfit = price.Sub(p.ApproximateAverageCost).Mul(quantity).Sub(feeInQuote) return profit, netProfit, true } } // handling short position, since Base here is negative we need to reverse the sign - p.ApproximateAverageCost = (p.ApproximateAverageCost.Mul(-p.Base) + quoteQuantity - feeInQuote).Div(-p.Base + quantity) - p.AverageCost = (p.AverageCost.Mul(-p.Base) + quoteQuantity).Div(-p.Base + quantity) - p.Base -= quantity - p.Quote += quoteQuantity - - return 0, 0, false + dividor := quantity.Sub(p.Base) + p.ApproximateAverageCost = p.ApproximateAverageCost.Mul(p.Base.Neg()). + Add(quoteQuantity). + Sub(feeInQuote). + Div(dividor) + p.AverageCost = p.AverageCost.Mul(p.Base.Neg()). + Add(quoteQuantity). + Div(dividor) + p.Base = p.Base.Sub(quantity) + p.Quote = p.Quote.Add(quoteQuantity) + + return fixedpoint.Zero, fixedpoint.Zero, false } - return 0, 0, false + return fixedpoint.Zero, fixedpoint.Zero, false } diff --git a/pkg/types/position_test.go b/pkg/types/position_test.go index f202d655ce..23867431ff 100644 --- a/pkg/types/position_test.go +++ b/pkg/types/position_test.go @@ -8,6 +8,8 @@ import ( "github.com/c9s/bbgo/pkg/fixedpoint" ) +const Delta = 1e-9 + func TestPosition_ExchangeFeeRate_Short(t *testing.T) { pos := &Position{ Symbol: "BTCUSDT", @@ -50,7 +52,7 @@ func TestPosition_ExchangeFeeRate_Short(t *testing.T) { expectedProfit := (averageCost-2000.0)*10.0 - (2000.0 * 10.0 * feeRate) assert.True(t, madeProfit) - assert.Equal(t, fixedpoint.NewFromFloat(expectedProfit), netProfit) + assert.InDelta(t, expectedProfit, netProfit.Float64(), Delta) } func TestPosition_ExchangeFeeRate_Long(t *testing.T) { @@ -95,18 +97,18 @@ func TestPosition_ExchangeFeeRate_Long(t *testing.T) { expectedProfit := (4000.0-averageCost)*10.0 - (4000.0 * 10.0 * feeRate) assert.True(t, madeProfit) - assert.Equal(t, fixedpoint.NewFromFloat(expectedProfit), netProfit) + assert.InDelta(t, expectedProfit, netProfit.Float64(), Delta) } func TestPosition(t *testing.T) { - var feeRate = 0.05 * 0.01 + var feeRate float64 = 0.05 * 0.01 var testcases = []struct { name string trades []Trade - expectedAverageCost fixedpoint.Value - expectedBase fixedpoint.Value - expectedQuote fixedpoint.Value - expectedProfit fixedpoint.Value + expectedAverageCost float64 + expectedBase float64 + expectedQuote float64 + expectedProfit float64 }{ { name: "base fee", @@ -120,10 +122,10 @@ func TestPosition(t *testing.T) { FeeCurrency: "BTC", }, }, - expectedAverageCost: fixedpoint.NewFromFloat((1000.0 * 0.01) / (0.01 * (1.0 - feeRate))), - expectedBase: fixedpoint.NewFromFloat(0.01 - (0.01 * feeRate)), - expectedQuote: fixedpoint.NewFromFloat(0 - 1000.0*0.01), - expectedProfit: fixedpoint.NewFromFloat(0.0), + expectedAverageCost: (1000.0 * 0.01) / (0.01 * (1.0 - feeRate)), + expectedBase: 0.01 - (0.01 * feeRate), + expectedQuote: 0 - 1000.0*0.01, + expectedProfit: 0.0, }, { name: "quote fee", @@ -137,10 +139,10 @@ func TestPosition(t *testing.T) { FeeCurrency: "USDT", }, }, - expectedAverageCost: fixedpoint.NewFromFloat((1000.0 * 0.01 * (1.0 - feeRate)) / 0.01), - expectedBase: fixedpoint.NewFromFloat(-0.01), - expectedQuote: fixedpoint.NewFromFloat(0 + 1000.0*0.01*(1.0-feeRate)), - expectedProfit: fixedpoint.NewFromFloat(0.0), + expectedAverageCost: (1000.0 * 0.01 * (1.0 - feeRate)) / 0.01, + expectedBase: -0.01, + expectedQuote: 0.0 + 1000.0 * 0.01 * (1.0 - feeRate), + expectedProfit: 0.0, }, { name: "long", @@ -158,10 +160,10 @@ func TestPosition(t *testing.T) { QuoteQuantity: 2000.0 * 0.03, }, }, - expectedAverageCost: fixedpoint.NewFromFloat((1000.0*0.01 + 2000.0*0.03) / 0.04), - expectedBase: fixedpoint.NewFromFloat(0.01 + 0.03), - expectedQuote: fixedpoint.NewFromFloat(0 - 1000.0*0.01 - 2000.0*0.03), - expectedProfit: fixedpoint.NewFromFloat(0.0), + expectedAverageCost: (1000.0*0.01 + 2000.0*0.03) / 0.04, + expectedBase: 0.01 + 0.03, + expectedQuote: 0 - 1000.0*0.01 - 2000.0*0.03, + expectedProfit: 0.0, }, { @@ -186,10 +188,10 @@ func TestPosition(t *testing.T) { QuoteQuantity: 3000.0 * 0.01, }, }, - expectedAverageCost: fixedpoint.NewFromFloat((1000.0*0.01 + 2000.0*0.03) / 0.04), - expectedBase: fixedpoint.NewFromFloat(0.03), - expectedQuote: fixedpoint.NewFromFloat(0 - 1000.0*0.01 - 2000.0*0.03 + 3000.0*0.01), - expectedProfit: fixedpoint.NewFromFloat((3000.0 - (1000.0*0.01+2000.0*0.03)/0.04) * 0.01), + expectedAverageCost: (1000.0*0.01 + 2000.0*0.03) / 0.04, + expectedBase: 0.03, + expectedQuote: 0 - 1000.0*0.01 - 2000.0*0.03 + 3000.0*0.01, + expectedProfit: (3000.0 - (1000.0*0.01+2000.0*0.03)/0.04) * 0.01, }, { @@ -215,10 +217,10 @@ func TestPosition(t *testing.T) { }, }, - expectedAverageCost: fixedpoint.NewFromFloat(3000.0), - expectedBase: fixedpoint.NewFromFloat(-0.06), - expectedQuote: fixedpoint.NewFromFloat(-1000.0*0.01 - 2000.0*0.03 + 3000.0*0.1), - expectedProfit: fixedpoint.NewFromFloat((3000.0 - (1000.0*0.01+2000.0*0.03)/0.04) * 0.04), + expectedAverageCost: 3000.0, + expectedBase: -0.06, + expectedQuote: -1000.0*0.01 - 2000.0*0.03 + 3000.0*0.1, + expectedProfit: (3000.0 - (1000.0*0.01+2000.0*0.03)/0.04) * 0.04, }, { @@ -238,10 +240,10 @@ func TestPosition(t *testing.T) { }, }, - expectedAverageCost: fixedpoint.NewFromFloat((2000.0*0.01 + 3000.0*0.03) / (0.01 + 0.03)), - expectedBase: fixedpoint.NewFromFloat(0 - 0.01 - 0.03), - expectedQuote: fixedpoint.NewFromFloat(2000.0*0.01 + 3000.0*0.03), - expectedProfit: fixedpoint.NewFromFloat(0.0), + expectedAverageCost: (2000.0*0.01 + 3000.0*0.03) / (0.01 + 0.03), + expectedBase: 0 - 0.01 - 0.03, + expectedQuote: 2000.0*0.01 + 3000.0*0.03, + expectedProfit: 0.0, }, } @@ -253,12 +255,11 @@ func TestPosition(t *testing.T) { QuoteCurrency: "USDT", } profitAmount, _, profit := pos.AddTrades(testcase.trades) - - assert.Equal(t, testcase.expectedQuote, pos.Quote, "expectedQuote") - assert.Equal(t, testcase.expectedBase, pos.Base, "expectedBase") - assert.Equal(t, testcase.expectedAverageCost, pos.AverageCost, "expectedAverageCost") + assert.InDelta(t, testcase.expectedQuote, pos.Quote.Float64(), Delta, "expectedQuote") + assert.InDelta(t, testcase.expectedBase, pos.Base.Float64(), Delta, "expectedBase") + assert.InDelta(t, testcase.expectedAverageCost, pos.AverageCost.Float64(), Delta, "expectedAverageCost") if profit { - assert.Equal(t, testcase.expectedProfit, profitAmount, "expectedProfit") + assert.InDelta(t, testcase.expectedProfit, profitAmount.Float64(), Delta, "expectedProfit") } }) } diff --git a/pkg/types/price_volume_heartbeat.go b/pkg/types/price_volume_heartbeat.go index 2e8703f1fb..51b4a5bfac 100644 --- a/pkg/types/price_volume_heartbeat.go +++ b/pkg/types/price_volume_heartbeat.go @@ -16,7 +16,7 @@ type PriceHeartBeat struct { // If the price is not updated (same price) and the last time exceeded the timeout, // Then false, and an error will be returned func (b *PriceHeartBeat) Update(pv PriceVolume, timeout time.Duration) (bool, error) { - if b.PriceVolume.Price == 0 || b.PriceVolume != pv { + if b.PriceVolume.Price.IsZero() || b.PriceVolume != pv { b.PriceVolume = pv b.LastTime = time.Now() return true, nil // successfully updated diff --git a/pkg/types/price_volume_slice.go b/pkg/types/price_volume_slice.go index b45c1577dc..7c0c8a60fd 100644 --- a/pkg/types/price_volume_slice.go +++ b/pkg/types/price_volume_slice.go @@ -13,19 +13,19 @@ type PriceVolume struct { } func (p PriceVolume) String() string { - return fmt.Sprintf("PriceVolume{ price: %f, volume: %f }", p.Price.Float64(), p.Volume.Float64()) + return fmt.Sprintf("PriceVolume{ price: %s, volume: %s }", p.Price.String(), p.Volume.String()) } type PriceVolumeSlice []PriceVolume func (slice PriceVolumeSlice) Len() int { return len(slice) } -func (slice PriceVolumeSlice) Less(i, j int) bool { return slice[i].Price < slice[j].Price } +func (slice PriceVolumeSlice) Less(i, j int) bool { return slice[i].Price.Compare(slice[j].Price) < 0 } func (slice PriceVolumeSlice) Swap(i, j int) { slice[i], slice[j] = slice[j], slice[i] } // Trim removes the pairs that volume = 0 func (slice PriceVolumeSlice) Trim() (pvs PriceVolumeSlice) { for _, pv := range slice { - if pv.Volume > 0 { + if pv.Volume.Sign() > 0 { pvs = append(pvs, pv) } } @@ -64,10 +64,10 @@ func (slice PriceVolumeSlice) First() (PriceVolume, bool) { } func (slice PriceVolumeSlice) IndexByVolumeDepth(requiredVolume fixedpoint.Value) int { - var tv fixedpoint.Value = 0 + var tv fixedpoint.Value = fixedpoint.Zero for x, el := range slice { - tv += el.Volume - if tv >= requiredVolume { + tv = tv.Add(el.Volume) + if tv.Compare(requiredVolume) >= 0 { return x } } @@ -84,7 +84,7 @@ func (slice PriceVolumeSlice) InsertAt(idx int, pv PriceVolume) PriceVolumeSlice func (slice PriceVolumeSlice) Remove(price fixedpoint.Value, descending bool) PriceVolumeSlice { matched, idx := slice.Find(price, descending) - if matched.Price != price || matched.Price == 0 { + if matched.Price.Compare(price) != 0 || matched.Price.IsZero() { return slice } @@ -98,12 +98,12 @@ func (slice PriceVolumeSlice) Remove(price fixedpoint.Value, descending bool) Pr func (slice PriceVolumeSlice) Find(price fixedpoint.Value, descending bool) (pv PriceVolume, 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 { + if idx >= len(slice) || slice[idx].Price.Compare(price) != 0 { return pv, idx } @@ -119,7 +119,7 @@ func (slice PriceVolumeSlice) Upsert(pv PriceVolume, descending bool) PriceVolum price := pv.Price _, idx := slice.Find(price, descending) - if idx >= len(slice) || slice[idx].Price != price { + if idx >= len(slice) || slice[idx].Price.Compare(price) != 0 { return slice.InsertAt(idx, pv) } @@ -142,7 +142,7 @@ func (slice *PriceVolumeSlice) UnmarshalJSON(b []byte) error { // [["9000", "10"], ["9900", "10"], ... ] // func ParsePriceVolumeSliceJSON(b []byte) (slice PriceVolumeSlice, err error) { - var as [][]interface{} + var as [][]fixedpoint.Value err = json.Unmarshal(b, &as) if err != nil { @@ -151,23 +151,14 @@ func ParsePriceVolumeSliceJSON(b []byte) (slice PriceVolumeSlice, err error) { for _, a := range as { var pv PriceVolume - price, err := fixedpoint.NewFromAny(a[0]) - if err != nil { - return slice, err - } - - volume, err := fixedpoint.NewFromAny(a[1]) - if err != nil { - return slice, err - } + pv.Price = a[0] + pv.Volume = a[1] // kucoin returns price in 0, we should skip - if price == 0 { + if pv.Price.Eq(fixedpoint.Zero) { continue } - pv.Price = price - pv.Volume = volume slice = append(slice, pv) } diff --git a/pkg/types/rbtorderbook.go b/pkg/types/rbtorderbook.go index eb5a04f7f9..d9fdd126ba 100644 --- a/pkg/types/rbtorderbook.go +++ b/pkg/types/rbtorderbook.go @@ -54,15 +54,15 @@ func (b *RBTOrderBook) BestAsk() (PriceVolume, bool) { func (b *RBTOrderBook) Spread() (fixedpoint.Value, bool) { bestBid, ok := b.BestBid() if !ok { - return 0, false + return fixedpoint.Zero, false } bestAsk, ok := b.BestAsk() if !ok { - return 0, false + return fixedpoint.Zero, false } - return bestAsk.Price - bestBid.Price, true + return bestAsk.Price.Sub(bestBid.Price), true } func (b *RBTOrderBook) IsValid() (bool, error) { @@ -77,8 +77,8 @@ func (b *RBTOrderBook) IsValid() (bool, error) { return false, errors.New("empty asks") } - if bid.Price > ask.Price { - return false, fmt.Errorf("bid price %f > ask price %f", bid.Price.Float64(), ask.Price.Float64()) + if bid.Price.Compare(ask.Price) > 0 { + return false, fmt.Errorf("bid price %s > ask price %s", bid.Price.String(), ask.Price.String()) } return true, nil @@ -102,7 +102,7 @@ func (b *RBTOrderBook) Reset() { func (b *RBTOrderBook) updateAsks(pvs PriceVolumeSlice) { for _, pv := range pvs { - if pv.Volume == 0 { + if pv.Volume.IsZero() { b.Asks.Delete(pv.Price) } else { b.Asks.Upsert(pv.Price, pv.Volume) @@ -112,7 +112,7 @@ func (b *RBTOrderBook) updateAsks(pvs PriceVolumeSlice) { func (b *RBTOrderBook) updateBids(pvs PriceVolumeSlice) { for _, pv := range pvs { - if pv.Volume == 0 { + if pv.Volume.IsZero() { b.Bids.Delete(pv.Price) } else { b.Bids.Upsert(pv.Price, pv.Volume) @@ -188,12 +188,12 @@ func (b *RBTOrderBook) SideBook(sideType SideType) PriceVolumeSlice { func (b *RBTOrderBook) Print() { b.Asks.Inorder(func(n *RBNode) bool { - fmt.Printf("ask: %f x %f", n.key.Float64(), n.value.Float64()) + fmt.Printf("ask: %s x %s", n.key.String(), n.value.String()) return true }) b.Bids.InorderReverse(func(n *RBNode) bool { - fmt.Printf("bid: %f x %f", n.key.Float64(), n.value.Float64()) + fmt.Printf("bid: %s x %s", n.key.String(), n.value.String()) return true }) } diff --git a/pkg/types/rbtree.go b/pkg/types/rbtree.go index 9a5d765de2..2c38c87959 100644 --- a/pkg/types/rbtree.go +++ b/pkg/types/rbtree.go @@ -162,7 +162,7 @@ func (tree *RBTree) Upsert(key, val fixedpoint.Value) { // found node, skip insert and fix x.value = val return - } else if node.key < x.key { + } else if node.key.Compare(x.key) < 0 { x = x.left } else { x = x.right @@ -173,7 +173,7 @@ func (tree *RBTree) Upsert(key, val fixedpoint.Value) { if y == neel { tree.Root = node - } else if node.key < y.key { + } else if node.key.Compare(y.key) < 0 { y.left = node } else { y.right = node @@ -197,7 +197,7 @@ func (tree *RBTree) Insert(key, val fixedpoint.Value) { for x != neel { y = x - if node.key < x.key { + if node.key.Compare(x.key) < 0 { x = x.left } else { x = x.right @@ -208,7 +208,7 @@ func (tree *RBTree) Insert(key, val fixedpoint.Value) { if y == neel { tree.Root = node - } else if node.key < y.key { + } else if node.key.Compare(y.key) < 0 { y.left = node } else { y.right = node @@ -221,7 +221,7 @@ func (tree *RBTree) Insert(key, val fixedpoint.Value) { func (tree *RBTree) Search(key fixedpoint.Value) *RBNode { var current = tree.Root for current != neel && key != current.key { - if key < current.key { + if key.Compare(current.key) < 0 { current = current.left } else { current = current.right diff --git a/pkg/types/sliceorderbook.go b/pkg/types/sliceorderbook.go index 671dabc306..c542e7284e 100644 --- a/pkg/types/sliceorderbook.go +++ b/pkg/types/sliceorderbook.go @@ -37,15 +37,15 @@ func (b *SliceOrderBook) LastUpdateTime() time.Time { func (b *SliceOrderBook) Spread() (fixedpoint.Value, bool) { bestBid, ok := b.BestBid() if !ok { - return 0, false + return fixedpoint.Zero, false } bestAsk, ok := b.BestAsk() if !ok { - return 0, false + return fixedpoint.Zero, false } - return bestAsk.Price - bestBid.Price, true + return bestAsk.Price.Sub(bestBid.Price), true } func (b *SliceOrderBook) BestBid() (PriceVolume, bool) { @@ -90,8 +90,8 @@ func (b *SliceOrderBook) IsValid() (bool, error) { return false, errors.New("empty asks") } - if bid.Price > ask.Price { - return false, fmt.Errorf("bid price %f > ask price %f", bid.Price.Float64(), ask.Price.Float64()) + if bid.Price.Compare(ask.Price) > 0 { + return false, fmt.Errorf("bid price %s > ask price %s", bid.Price.String(), ask.Price.String()) } return true, nil @@ -112,7 +112,7 @@ func (b *SliceOrderBook) PriceVolumesBySide(side SideType) PriceVolumeSlice { func (b *SliceOrderBook) updateAsks(pvs PriceVolumeSlice) { for _, pv := range pvs { - if pv.Volume == 0 { + if pv.Volume.IsZero() { b.Asks = b.Asks.Remove(pv.Price, false) } else { b.Asks = b.Asks.Upsert(pv, false) @@ -122,7 +122,7 @@ func (b *SliceOrderBook) updateAsks(pvs PriceVolumeSlice) { func (b *SliceOrderBook) updateBids(pvs PriceVolumeSlice) { for _, pv := range pvs { - if pv.Volume == 0 { + if pv.Volume.IsZero() { b.Bids = b.Bids.Remove(pv.Price, true) } else { b.Bids = b.Bids.Upsert(pv, true) diff --git a/pkg/types/trade.go b/pkg/types/trade.go index 21c3a8e4b5..4fb910fdd5 100644 --- a/pkg/types/trade.go +++ b/pkg/types/trade.go @@ -55,16 +55,16 @@ type Trade struct { ID uint64 `json:"id" db:"id"` OrderID uint64 `json:"orderID" db:"order_id"` Exchange ExchangeName `json:"exchange" db:"exchange"` - Price float64 `json:"price" db:"price"` - Quantity float64 `json:"quantity" db:"quantity"` - QuoteQuantity float64 `json:"quoteQuantity" db:"quote_quantity"` + Price fixedpoint.Value `json:"price" db:"price"` + Quantity fixedpoint.Value `json:"quantity" db:"quantity"` + QuoteQuantity fixedpoint.Value `json:"quoteQuantity" db:"quote_quantity"` Symbol string `json:"symbol" db:"symbol"` Side SideType `json:"side" db:"side"` IsBuyer bool `json:"isBuyer" db:"is_buyer"` IsMaker bool `json:"isMaker" db:"is_maker"` Time Time `json:"tradedAt" db:"traded_at"` - Fee float64 `json:"fee" db:"fee"` + Fee fixedpoint.Value `json:"fee" db:"fee"` FeeCurrency string `json:"feeCurrency" db:"fee_currency"` IsMargin bool `json:"isMargin" db:"is_margin"` @@ -76,18 +76,18 @@ type Trade struct { } func (trade Trade) PositionChange() fixedpoint.Value { - q := fixedpoint.NewFromFloat(trade.Quantity) + q := trade.Quantity switch trade.Side { case SideTypeSell: - return -q + return q.Neg() case SideTypeBuy: return q case SideTypeSelf: - return 0 + return fixedpoint.Zero } - return 0 + return fixedpoint.Zero } func trimTrailingZero(a string) string { @@ -121,10 +121,10 @@ func (trade Trade) String() string { trade.Exchange.String(), trade.Symbol, trade.Side, - trimTrailingZeroFloat(trade.Quantity), - trimTrailingZeroFloat(trade.Price), - trimTrailingZeroFloat(trade.QuoteQuantity), - trimTrailingZeroFloat(trade.Fee), + trimTrailingZeroFloat(trade.Quantity.Float64()), + trimTrailingZeroFloat(trade.Price.Float64()), + trimTrailingZeroFloat(trade.QuoteQuantity.Float64()), + trimTrailingZeroFloat(trade.Fee.Float64()), trade.FeeCurrency, trade.OrderID, trade.Time.Time().Format(time.StampMilli), @@ -137,10 +137,10 @@ func (trade Trade) PlainText() string { trade.Exchange.String(), trade.Symbol, trade.Side, - trimTrailingZeroFloat(trade.Quantity), - trimTrailingZeroFloat(trade.Price), - trimTrailingZeroFloat(trade.QuoteQuantity), - trimTrailingZeroFloat(trade.Fee), + trimTrailingZeroFloat(trade.Quantity.Float64()), + trimTrailingZeroFloat(trade.Price.Float64()), + trimTrailingZeroFloat(trade.QuoteQuantity.Float64()), + trimTrailingZeroFloat(trade.Fee.Float64()), trade.FeeCurrency) } @@ -184,10 +184,10 @@ func (trade Trade) SlackAttachment() slack.Attachment { Color: color, Fields: []slack.AttachmentField{ {Title: "Exchange", Value: trade.Exchange.String(), Short: true}, - {Title: "Price", Value: trimTrailingZeroFloat(trade.Price), Short: true}, - {Title: "Quantity", Value: trimTrailingZeroFloat(trade.Quantity), Short: true}, - {Title: "QuoteQuantity", Value: trimTrailingZeroFloat(trade.QuoteQuantity), Short: true}, - {Title: "Fee", Value: trimTrailingZeroFloat(trade.Fee), Short: true}, + {Title: "Price", Value: trimTrailingZeroFloat(trade.Price.Float64()), Short: true}, + {Title: "Quantity", Value: trimTrailingZeroFloat(trade.Quantity.Float64()), Short: true}, + {Title: "QuoteQuantity", Value: trimTrailingZeroFloat(trade.QuoteQuantity.Float64()), Short: true}, + {Title: "Fee", Value: trimTrailingZeroFloat(trade.Fee.Float64()), Short: true}, {Title: "FeeCurrency", Value: trade.FeeCurrency, Short: true}, {Title: "Liquidity", Value: liquidity, Short: true}, {Title: "Order ID", Value: strconv.FormatUint(trade.OrderID, 10), Short: true}, diff --git a/pkg/util/math.go b/pkg/util/math.go index f9b984027e..04c457d46a 100644 --- a/pkg/util/math.go +++ b/pkg/util/math.go @@ -3,6 +3,7 @@ package util import ( "math" "strconv" + "github.com/c9s/bbgo/pkg/fixedpoint" ) const MaxDigits = 18 // MAX_INT64 ~ 9 * 10^18 @@ -18,6 +19,10 @@ func Pow10(n int64) int64 { return Pow10Table[n] } +func FormatValue(val fixedpoint.Value, prec int) string { + return strconv.FormatFloat(val.Float64(), 'f', prec, 64) +} + func FormatFloat(val float64, prec int) string { return strconv.FormatFloat(val, 'f', prec, 64) }