diff --git a/pkg/accounting/pnl/report.go b/pkg/accounting/pnl/report.go index 46ebad08d3..5831f47354 100644 --- a/pkg/accounting/pnl/report.go +++ b/pkg/accounting/pnl/report.go @@ -70,8 +70,8 @@ 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.Float64()), Short: true}, - {Title: "Average Cost", Value: report.Market.FormatPrice(report.AverageCost.Float64()), Short: true}, + {Title: "Current Price", Value: report.Market.FormatPrice(report.LastPrice), Short: true}, + {Title: "Average Cost", Value: report.Market.FormatPrice(report.AverageCost), Short: true}, // FIXME: // {Title: "Fee (USD)", Value: types.USD.FormatMoney(report.FeeInUSD), Short: true}, diff --git a/pkg/bbgo/config.go b/pkg/bbgo/config.go index e0ca7dd23a..4d83269756 100644 --- a/pkg/bbgo/config.go +++ b/pkg/bbgo/config.go @@ -124,7 +124,7 @@ func (m BacktestAccountBalanceMap) BalanceMap() types.BalanceMap { balances[currency] = types.Balance{ Currency: currency, Available: value, - Locked: 0, + Locked: fixedpoint.Zero, } } return balances diff --git a/pkg/exchange/binance/convert.go b/pkg/exchange/binance/convert.go index f6df8c48bc..b589b8f8a8 100644 --- a/pkg/exchange/binance/convert.go +++ b/pkg/exchange/binance/convert.go @@ -2,7 +2,6 @@ package binance import ( "fmt" - "strconv" "strings" "time" @@ -12,7 +11,6 @@ import ( "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" - "github.com/c9s/bbgo/pkg/util" ) func toGlobalMarket(symbol binance.Symbol) types.Market { @@ -26,8 +24,8 @@ func toGlobalMarket(symbol binance.Symbol) types.Market { } if f := symbol.MinNotionalFilter(); f != nil { - market.MinNotional = util.MustParseFloat(f.MinNotional) - market.MinAmount = util.MustParseFloat(f.MinNotional) + market.MinNotional = fixedpoint.MustNewFromString(f.MinNotional) + market.MinAmount = fixedpoint.MustNewFromString(f.MinNotional) } // The LOT_SIZE filter defines the quantity (aka "lots" in auction terms) rules for a symbol. @@ -36,15 +34,15 @@ func toGlobalMarket(symbol binance.Symbol) types.Market { // maxQty defines the maximum quantity/icebergQty allowed. // stepSize defines the intervals that a quantity/icebergQty can be increased/decreased by. if f := symbol.LotSizeFilter(); f != nil { - market.MinQuantity = util.MustParseFloat(f.MinQuantity) - market.MaxQuantity = util.MustParseFloat(f.MaxQuantity) - market.StepSize = util.MustParseFloat(f.StepSize) + market.MinQuantity = fixedpoint.MustNewFromString(f.MinQuantity) + market.MaxQuantity = fixedpoint.MustNewFromString(f.MaxQuantity) + market.StepSize = fixedpoint.MustNewFromString(f.StepSize) } if f := symbol.PriceFilter(); f != nil { - market.MaxPrice = util.MustParseFloat(f.MaxPrice) - market.MinPrice = util.MustParseFloat(f.MinPrice) - market.TickSize = util.MustParseFloat(f.TickSize) + market.MaxPrice = fixedpoint.MustNewFromString(f.MaxPrice) + market.MinPrice = fixedpoint.MustNewFromString(f.MinPrice) + market.TickSize = fixedpoint.MustNewFromString(f.TickSize) } return market @@ -62,8 +60,8 @@ func toGlobalFuturesMarket(symbol futures.Symbol) types.Market { } if f := symbol.MinNotionalFilter(); f != nil { - market.MinNotional = util.MustParseFloat(f.Notional) - market.MinAmount = util.MustParseFloat(f.Notional) + market.MinNotional = fixedpoint.MustNewFromString(f.Notional) + market.MinAmount = fixedpoint.MustNewFromString(f.Notional) } // The LOT_SIZE filter defines the quantity (aka "lots" in auction terms) rules for a symbol. @@ -72,15 +70,15 @@ func toGlobalFuturesMarket(symbol futures.Symbol) types.Market { // maxQty defines the maximum quantity/icebergQty allowed. // stepSize defines the intervals that a quantity/icebergQty can be increased/decreased by. if f := symbol.LotSizeFilter(); f != nil { - market.MinQuantity = util.MustParseFloat(f.MinQuantity) - market.MaxQuantity = util.MustParseFloat(f.MaxQuantity) - market.StepSize = util.MustParseFloat(f.StepSize) + market.MinQuantity = fixedpoint.MustNewFromString(f.MinQuantity) + market.MaxQuantity = fixedpoint.MustNewFromString(f.MaxQuantity) + market.StepSize = fixedpoint.MustNewFromString(f.StepSize) } if f := symbol.PriceFilter(); f != nil { - market.MaxPrice = util.MustParseFloat(f.MaxPrice) - market.MinPrice = util.MustParseFloat(f.MinPrice) - market.TickSize = util.MustParseFloat(f.TickSize) + market.MaxPrice = fixedpoint.MustNewFromString(f.MaxPrice) + market.MinPrice = fixedpoint.MustNewFromString(f.MinPrice) + market.TickSize = fixedpoint.MustNewFromString(f.TickSize) } return market @@ -236,13 +234,13 @@ func toGlobalFuturesUserAssets(assets []*futures.AccountAsset) (retAssets types. func toGlobalTicker(stats *binance.PriceChangeStats) (*types.Ticker, error) { return &types.Ticker{ - Volume: util.MustParseFloat(stats.Volume), - Last: util.MustParseFloat(stats.LastPrice), - Open: util.MustParseFloat(stats.OpenPrice), - High: util.MustParseFloat(stats.HighPrice), - Low: util.MustParseFloat(stats.LowPrice), - Buy: util.MustParseFloat(stats.BidPrice), - Sell: util.MustParseFloat(stats.AskPrice), + Volume: fixedpoint.MustNewFromString(stats.Volume), + Last: fixedpoint.MustNewFromString(stats.LastPrice), + Open: fixedpoint.MustNewFromString(stats.OpenPrice), + High: fixedpoint.MustNewFromString(stats.HighPrice), + Low: fixedpoint.MustNewFromString(stats.LowPrice), + Buy: fixedpoint.MustNewFromString(stats.BidPrice), + Sell: fixedpoint.MustNewFromString(stats.AskPrice), Time: time.Unix(0, stats.CloseTime*int64(time.Millisecond)), }, nil } @@ -324,15 +322,15 @@ func toGlobalOrder(binanceOrder *binance.Order, isMargin bool) (*types.Order, er Symbol: binanceOrder.Symbol, Side: toGlobalSideType(binanceOrder.Side), Type: toGlobalOrderType(binanceOrder.Type), - Quantity: util.MustParseFloat(binanceOrder.OrigQuantity), - Price: util.MustParseFloat(binanceOrder.Price), + Quantity: fixedpoint.MustNewFromString(binanceOrder.OrigQuantity), + Price: fixedpoint.MustNewFromString(binanceOrder.Price), TimeInForce: string(binanceOrder.TimeInForce), }, Exchange: types.ExchangeBinance, IsWorking: binanceOrder.IsWorking, OrderID: uint64(binanceOrder.OrderID), Status: toGlobalOrderStatus(binanceOrder.Status), - ExecutedQuantity: util.MustParseFloat(binanceOrder.ExecutedQuantity), + ExecutedQuantity: fixedpoint.MustNewFromString(binanceOrder.ExecutedQuantity), CreationTime: types.Time(millisecondTime(binanceOrder.Time)), UpdateTime: types.Time(millisecondTime(binanceOrder.UpdateTime)), IsMargin: isMargin, @@ -349,14 +347,14 @@ func toGlobalFuturesOrder(futuresOrder *futures.Order, isMargin bool) (*types.Or Type: toGlobalFuturesOrderType(futuresOrder.Type), ReduceOnly: futuresOrder.ReduceOnly, ClosePosition: futuresOrder.ClosePosition, - Quantity: util.MustParseFloat(futuresOrder.OrigQuantity), - Price: util.MustParseFloat(futuresOrder.Price), + Quantity: fixedpoint.MustNewFromString(futuresOrder.OrigQuantity), + Price: fixedpoint.MustNewFromString(futuresOrder.Price), TimeInForce: string(futuresOrder.TimeInForce), }, Exchange: types.ExchangeBinance, OrderID: uint64(futuresOrder.OrderID), Status: toGlobalFuturesOrderStatus(futuresOrder.Status), - ExecutedQuantity: util.MustParseFloat(futuresOrder.ExecutedQuantity), + ExecutedQuantity: fixedpoint.MustNewFromString(futuresOrder.ExecutedQuantity), CreationTime: types.Time(millisecondTime(futuresOrder.Time)), UpdateTime: types.Time(millisecondTime(futuresOrder.UpdateTime)), IsMargin: isMargin, @@ -376,27 +374,27 @@ func toGlobalTrade(t binance.TradeV3, isMargin bool) (*types.Trade, error) { side = types.SideTypeSell } - price, err := strconv.ParseFloat(t.Price, 64) + price, err := fixedpoint.NewFromString(t.Price) if err != nil { return nil, errors.Wrapf(err, "price parse error, price: %+v", t.Price) } - quantity, err := strconv.ParseFloat(t.Quantity, 64) + quantity, err := fixedpoint.NewFromString(t.Quantity) if err != nil { return nil, errors.Wrapf(err, "quantity parse error, quantity: %+v", t.Quantity) } - var quoteQuantity = 0.0 + var quoteQuantity fixedpoint.Value if len(t.QuoteQuantity) > 0 { - quoteQuantity, err = strconv.ParseFloat(t.QuoteQuantity, 64) + quoteQuantity, err = fixedpoint.NewFromString(t.QuoteQuantity) if err != nil { return nil, errors.Wrapf(err, "quote quantity parse error, quoteQuantity: %+v", t.QuoteQuantity) } } else { - quoteQuantity = price * quantity + quoteQuantity = price.Mul(quantity) } - fee, err := strconv.ParseFloat(t.Commission, 64) + fee, err := fixedpoint.NewFromString(t.Commission) if err != nil { return nil, errors.Wrapf(err, "commission parse error, commission: %+v", t.Commission) } @@ -429,27 +427,27 @@ func toGlobalFuturesTrade(t futures.AccountTrade) (*types.Trade, error) { side = types.SideTypeSell } - price, err := strconv.ParseFloat(t.Price, 64) + price, err := fixedpoint.NewFromString(t.Price) if err != nil { return nil, errors.Wrapf(err, "price parse error, price: %+v", t.Price) } - quantity, err := strconv.ParseFloat(t.Quantity, 64) + quantity, err := fixedpoint.NewFromString(t.Quantity) if err != nil { return nil, errors.Wrapf(err, "quantity parse error, quantity: %+v", t.Quantity) } - var quoteQuantity = 0.0 + var quoteQuantity fixedpoint.Value if len(t.QuoteQuantity) > 0 { - quoteQuantity, err = strconv.ParseFloat(t.QuoteQuantity, 64) + quoteQuantity, err = fixedpoint.NewFromString(t.QuoteQuantity) if err != nil { return nil, errors.Wrapf(err, "quote quantity parse error, quoteQuantity: %+v", t.QuoteQuantity) } } else { - quoteQuantity = price * quantity + quoteQuantity = price.Mul(quantity) } - fee, err := strconv.ParseFloat(t.Commission, 64) + fee, err := fixedpoint.NewFromString(t.Commission) if err != nil { return nil, errors.Wrapf(err, "commission parse error, commission: %+v", t.Commission) } diff --git a/pkg/exchange/binance/exchange.go b/pkg/exchange/binance/exchange.go index bd3fa95c42..3d4bd1e559 100644 --- a/pkg/exchange/binance/exchange.go +++ b/pkg/exchange/binance/exchange.go @@ -24,7 +24,6 @@ import ( "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" - "github.com/c9s/bbgo/pkg/util" ) const BNB = "BNB" @@ -152,13 +151,13 @@ func (e *Exchange) QueryTickers(ctx context.Context, symbol ...string) (map[stri } tick := types.Ticker{ - Volume: util.MustParseFloat(stats.Volume), - Last: util.MustParseFloat(stats.LastPrice), - Open: util.MustParseFloat(stats.OpenPrice), - High: util.MustParseFloat(stats.HighPrice), - Low: util.MustParseFloat(stats.LowPrice), - Buy: util.MustParseFloat(stats.BidPrice), - Sell: util.MustParseFloat(stats.AskPrice), + Volume: fixedpoint.MustNewFromString(stats.Volume), + Last: fixedpoint.MustNewFromString(stats.LastPrice), + Open: fixedpoint.MustNewFromString(stats.OpenPrice), + High: fixedpoint.MustNewFromString(stats.HighPrice), + Low: fixedpoint.MustNewFromString(stats.LowPrice), + Buy: fixedpoint.MustNewFromString(stats.BidPrice), + Sell: fixedpoint.MustNewFromString(stats.AskPrice), Time: time.Unix(0, stats.CloseTime*int64(time.Millisecond)), } @@ -197,13 +196,13 @@ func (e *Exchange) QueryMarkets(ctx context.Context) (types.MarketMap, error) { return markets, nil } -func (e *Exchange) QueryAveragePrice(ctx context.Context, symbol string) (float64, error) { +func (e *Exchange) QueryAveragePrice(ctx context.Context, symbol string) (fixedpoint.Value, error) { resp, err := e.Client.NewAveragePriceService().Symbol(symbol).Do(ctx) if err != nil { - return 0, err + return fixedpoint.Zero, err } - return util.MustParseFloat(resp.Price), nil + return fixedpoint.MustNewFromString(resp.Price), nil } func (e *Exchange) NewStream() types.Stream { @@ -342,10 +341,10 @@ func (e *Exchange) QueryWithdrawHistory(ctx context.Context, asset string, since Exchange: types.ExchangeBinance, ApplyTime: types.Time(applyTime), Asset: d.Coin, - Amount: util.MustParseFloat(d.Amount), + Amount: fixedpoint.MustNewFromString(d.Amount), Address: d.Address, TransactionID: d.TxID, - TransactionFee: util.MustParseFloat(d.TransactionFee), + TransactionFee: fixedpoint.MustNewFromString(d.TransactionFee), WithdrawOrderID: d.WithdrawOrderID, Network: d.Network, Status: status, @@ -414,7 +413,7 @@ func (e *Exchange) QueryDepositHistory(ctx context.Context, asset string, since, Exchange: types.ExchangeBinance, Time: types.Time(time.Unix(0, d.InsertTime*int64(time.Millisecond))), Asset: d.Coin, - Amount: util.MustParseFloat(d.Amount), + Amount: fixedpoint.MustNewFromString(d.Amount), Address: d.Address, AddressTag: d.AddressTag, TransactionID: d.TxID, @@ -754,7 +753,8 @@ func (e *Exchange) submitMarginOrder(ctx context.Context, order types.SubmitOrde if order.Market.Symbol != "" { req.Quantity(order.Market.FormatQuantity(order.Quantity)) } else { - req.Quantity(strconv.FormatFloat(order.Quantity, 'f', 8, 64)) + // TODO report error + req.Quantity(order.Quantity.FormatString(8)) } // set price field for limit orders @@ -763,7 +763,8 @@ func (e *Exchange) submitMarginOrder(ctx context.Context, order types.SubmitOrde if order.Market.Symbol != "" { req.Price(order.Market.FormatPrice(order.Price)) } else { - req.Price(strconv.FormatFloat(order.Price, 'f', 8, 64)) + // TODO report error + req.Price(order.Price.FormatString(8)) } } @@ -774,7 +775,8 @@ func (e *Exchange) submitMarginOrder(ctx context.Context, order types.SubmitOrde if order.Market.Symbol != "" { req.StopPrice(order.Market.FormatPrice(order.StopPrice)) } else { - req.StopPrice(strconv.FormatFloat(order.StopPrice, 'f', 8, 64)) + // TODO report error + req.StopPrice(order.StopPrice.FormatString(8)) } } @@ -838,7 +840,8 @@ func (e *Exchange) submitFuturesOrder(ctx context.Context, order types.SubmitOrd if order.Market.Symbol != "" { req.Quantity(order.Market.FormatQuantity(order.Quantity)) } else { - req.Quantity(strconv.FormatFloat(order.Quantity, 'f', 8, 64)) + // TODO report error + req.Quantity(order.Quantity.FormatString(8)) } // set price field for limit orders @@ -847,7 +850,8 @@ func (e *Exchange) submitFuturesOrder(ctx context.Context, order types.SubmitOrd if order.Market.Symbol != "" { req.Price(order.Market.FormatPrice(order.Price)) } else { - req.Price(strconv.FormatFloat(order.Price, 'f', 8, 64)) + // TODO report error + req.Price(order.Price.FormatString(8)) } } @@ -858,7 +862,8 @@ func (e *Exchange) submitFuturesOrder(ctx context.Context, order types.SubmitOrd if order.Market.Symbol != "" { req.StopPrice(order.Market.FormatPrice(order.StopPrice)) } else { - req.StopPrice(strconv.FormatFloat(order.StopPrice, 'f', 8, 64)) + // TODO report error + req.StopPrice(order.StopPrice.FormatString(8)) } } @@ -975,7 +980,8 @@ func (e *Exchange) submitSpotOrder(ctx context.Context, order types.SubmitOrder) if order.Market.Symbol != "" { req.Quantity(order.Market.FormatQuantity(order.Quantity)) } else { - req.Quantity(strconv.FormatFloat(order.Quantity, 'f', 8, 64)) + // TODO: report error + req.Quantity(order.Quantity.FormatString(8)) } // set price field for limit orders @@ -984,7 +990,8 @@ func (e *Exchange) submitSpotOrder(ctx context.Context, order types.SubmitOrder) if order.Market.Symbol != "" { req.Price(order.Market.FormatPrice(order.Price)) } else { - req.Price(strconv.FormatFloat(order.Price, 'f', 8, 64)) + // TODO: report error + req.Price(order.Price.FormatString(8)) } } @@ -993,7 +1000,8 @@ func (e *Exchange) submitSpotOrder(ctx context.Context, order types.SubmitOrder) if order.Market.Symbol != "" { req.StopPrice(order.Market.FormatPrice(order.StopPrice)) } else { - req.StopPrice(strconv.FormatFloat(order.StopPrice, 'f', 8, 64)) + // TODO: report error + req.StopPrice(order.StopPrice.FormatString(8)) } } @@ -1111,14 +1119,14 @@ func (e *Exchange) QueryKLines(ctx context.Context, symbol string, interval type Interval: interval, StartTime: types.NewTimeFromUnix(0, k.OpenTime*int64(time.Millisecond)), EndTime: types.NewTimeFromUnix(0, k.CloseTime*int64(time.Millisecond)), - Open: util.MustParseFloat(k.Open), - Close: util.MustParseFloat(k.Close), - High: util.MustParseFloat(k.High), - Low: util.MustParseFloat(k.Low), - Volume: util.MustParseFloat(k.Volume), - QuoteVolume: util.MustParseFloat(k.QuoteAssetVolume), - TakerBuyBaseAssetVolume: util.MustParseFloat(k.TakerBuyBaseAssetVolume), - TakerBuyQuoteAssetVolume: util.MustParseFloat(k.TakerBuyQuoteAssetVolume), + Open: fixedpoint.MustNewFromString(k.Open), + Close: fixedpoint.MustNewFromString(k.Close), + High: fixedpoint.MustNewFromString(k.High), + Low: fixedpoint.MustNewFromString(k.Low), + Volume: fixedpoint.MustNewFromString(k.Volume), + QuoteVolume: fixedpoint.MustNewFromString(k.QuoteAssetVolume), + TakerBuyBaseAssetVolume: fixedpoint.MustNewFromString(k.TakerBuyBaseAssetVolume), + TakerBuyQuoteAssetVolume: fixedpoint.MustNewFromString(k.TakerBuyQuoteAssetVolume), LastTradeID: 0, NumberOfTrades: uint64(k.TradeNum), Closed: true, diff --git a/pkg/exchange/binance/parse.go b/pkg/exchange/binance/parse.go index 84f36f103d..ab57866ac9 100644 --- a/pkg/exchange/binance/parse.go +++ b/pkg/exchange/binance/parse.go @@ -119,13 +119,13 @@ func (e *ExecutionReportEvent) Order() (*types.Order, error) { ClientOrderID: e.ClientOrderID, Side: toGlobalSideType(binance.SideType(e.Side)), Type: toGlobalOrderType(binance.OrderType(e.OrderType)), - Quantity: e.OrderQuantity.Float64(), - Price: e.OrderPrice.Float64(), + Quantity: e.OrderQuantity, + Price: e.OrderPrice, TimeInForce: e.TimeInForce, }, OrderID: uint64(e.OrderID), Status: toGlobalOrderStatus(binance.OrderStatusType(e.CurrentOrderStatus)), - ExecutedQuantity: e.CumulativeFilledQuantity.Float64(), + ExecutedQuantity: e.CumulativeFilledQuantity, CreationTime: types.Time(orderCreationTime), }, nil } @@ -142,13 +142,13 @@ func (e *ExecutionReportEvent) Trade() (*types.Trade, error) { Symbol: e.Symbol, OrderID: uint64(e.OrderID), Side: toGlobalSideType(binance.SideType(e.Side)), - Price: e.LastExecutedPrice.Float64(), - Quantity: e.LastExecutedQuantity.Float64(), - QuoteQuantity: e.LastQuoteAssetTransactedQuantity.Float64(), + Price: e.LastExecutedPrice, + Quantity: e.LastExecutedQuantity, + QuoteQuantity: e.LastQuoteAssetTransactedQuantity, IsBuyer: e.Side == "BUY", IsMaker: e.IsMaker, Time: types.Time(tt), - Fee: e.CommissionAmount.Float64(), + Fee: e.CommissionAmount, FeeCurrency: e.CommissionAsset, }, nil } @@ -528,14 +528,14 @@ func (k *KLine) KLine() types.KLine { Interval: types.Interval(k.Interval), StartTime: types.NewTimeFromUnix(0, k.StartTime*int64(time.Millisecond)), EndTime: types.NewTimeFromUnix(0, k.EndTime*int64(time.Millisecond)), - Open: k.Open.Float64(), - Close: k.Close.Float64(), - High: k.High.Float64(), - Low: k.Low.Float64(), - Volume: k.Volume.Float64(), - QuoteVolume: k.QuoteVolume.Float64(), - TakerBuyBaseAssetVolume: k.TakerBuyBaseAssetVolume.Float64(), - TakerBuyQuoteAssetVolume: k.TakerBuyQuoteAssetVolume.Float64(), + Open: k.Open, + Close: k.Close, + High: k.High, + Low: k.Low, + Volume: k.Volume, + QuoteVolume: k.QuoteVolume, + TakerBuyBaseAssetVolume: k.TakerBuyBaseAssetVolume, + TakerBuyQuoteAssetVolume: k.TakerBuyQuoteAssetVolume, LastTradeID: uint64(k.LastTradeID), NumberOfTrades: uint64(k.NumberOfTrades), Closed: k.Closed, @@ -708,13 +708,13 @@ func (e *OrderTradeUpdateEvent) OrderFutures() (*types.Order, error) { ClientOrderID: e.OrderTrade.ClientOrderID, Side: toGlobalFuturesSideType(futures.SideType(e.OrderTrade.Side)), Type: toGlobalFuturesOrderType(futures.OrderType(e.OrderTrade.OrderType)), - Quantity: e.OrderTrade.OriginalQuantity.Float64(), - Price: e.OrderTrade.OriginalPrice.Float64(), + Quantity: e.OrderTrade.OriginalQuantity, + Price: e.OrderTrade.OriginalPrice, TimeInForce: e.OrderTrade.TimeInForce, }, OrderID: uint64(e.OrderTrade.OrderId), Status: toGlobalFuturesOrderStatus(futures.OrderStatusType(e.OrderTrade.CurrentOrderStatus)), - ExecutedQuantity: e.OrderTrade.OrderFilledAccumulatedQuantity.Float64(), + ExecutedQuantity: e.OrderTrade.OrderFilledAccumulatedQuantity, CreationTime: types.Time(orderCreationTime), }, nil } @@ -731,13 +731,13 @@ func (e *OrderTradeUpdateEvent) TradeFutures() (*types.Trade, error) { Symbol: e.OrderTrade.Symbol, OrderID: uint64(e.OrderTrade.OrderId), Side: toGlobalSideType(binance.SideType(e.OrderTrade.Side)), - Price: e.OrderTrade.LastFilledPrice.Float64(), - Quantity: e.OrderTrade.OrderLastFilledQuantity.Float64(), - QuoteQuantity: e.OrderTrade.OrderFilledAccumulatedQuantity.Float64(), + Price: e.OrderTrade.LastFilledPrice, + Quantity: e.OrderTrade.OrderLastFilledQuantity, + QuoteQuantity: e.OrderTrade.OrderFilledAccumulatedQuantity, IsBuyer: e.OrderTrade.Side == "BUY", IsMaker: e.OrderTrade.IsMaker, Time: types.Time(tt), - Fee: e.OrderTrade.CommissionAmount.Float64(), + Fee: e.OrderTrade.CommissionAmount, FeeCurrency: e.OrderTrade.CommissionAsset, }, nil } diff --git a/pkg/exchange/ftx/convert.go b/pkg/exchange/ftx/convert.go index b67e72c89c..d8583656d1 100644 --- a/pkg/exchange/ftx/convert.go +++ b/pkg/exchange/ftx/convert.go @@ -7,7 +7,6 @@ import ( log "github.com/sirupsen/logrus" - "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" ) @@ -64,14 +63,14 @@ func toGlobalOrder(r order) (types.Order, error) { case "new": o.Status = types.OrderStatusNew case "open": - if fixedpoint.NewFromFloat(o.ExecutedQuantity) != fixedpoint.NewFromInt(0) { + if !o.ExecutedQuantity.IsZero() { o.Status = types.OrderStatusPartiallyFilled } else { o.Status = types.OrderStatusNew } case "closed": // filled or canceled - if fixedpoint.NewFromFloat(o.Quantity) == fixedpoint.NewFromFloat(o.ExecutedQuantity) { + if o.Quantity == o.ExecutedQuantity { o.Status = types.OrderStatusFilled } else { // can't distinguish it's canceled or rejected from order response, so always set to canceled @@ -124,7 +123,7 @@ func toGlobalTrade(f fill) (types.Trade, error) { Exchange: types.ExchangeFTX, Price: f.Price, Quantity: f.Size, - QuoteQuantity: f.Price * f.Size, + QuoteQuantity: f.Price.Mul(f.Size), Symbol: toGlobalSymbol(f.Market), Side: f.Side, IsBuyer: f.Side == types.SideTypeBuy, diff --git a/pkg/exchange/ftx/exchange.go b/pkg/exchange/ftx/exchange.go index c7faec912f..ca7184f9cd 100644 --- a/pkg/exchange/ftx/exchange.go +++ b/pkg/exchange/ftx/exchange.go @@ -38,10 +38,10 @@ type Exchange struct { type MarketTicker struct { Market types.Market - Price float64 - Ask float64 - Bid float64 - Last float64 + Price fixedpoint.Value + Ask fixedpoint.Value + Bid fixedpoint.Value + Last fixedpoint.Value } type MarketMap map[string]MarketTicker @@ -138,19 +138,19 @@ func (e *Exchange) _queryMarkets(ctx context.Context) (MarketMap, error) { LocalSymbol: m.Name, // The max precision is length(DefaultPow). For example, currently fixedpoint.DefaultPow // is 1e8, so the max precision will be 8. - PricePrecision: fixedpoint.NumFractionalDigits(fixedpoint.NewFromFloat(m.PriceIncrement)), - VolumePrecision: fixedpoint.NumFractionalDigits(fixedpoint.NewFromFloat(m.SizeIncrement)), + PricePrecision: fixedpoint.NumFractionalDigits(m.PriceIncrement), + VolumePrecision: fixedpoint.NumFractionalDigits(m.SizeIncrement), QuoteCurrency: toGlobalCurrency(m.QuoteCurrency), BaseCurrency: toGlobalCurrency(m.BaseCurrency), // FTX only limit your order by `MinProvideSize`, so I assign zero value to unsupported fields: // MinNotional, MinAmount, MaxQuantity, MinPrice and MaxPrice. - MinNotional: 0, - MinAmount: 0, + MinNotional: fixedpoint.Zero, + MinAmount: fixedpoint.Zero, MinQuantity: m.MinProvideSize, - MaxQuantity: 0, + MaxQuantity: fixedpoint.Zero, StepSize: m.SizeIncrement, - MinPrice: 0, - MaxPrice: 0, + MinPrice: fixedpoint.Zero, + MaxPrice: fixedpoint.Zero, TickSize: m.PriceIncrement, }, Price: m.Price, @@ -173,9 +173,9 @@ func (e *Exchange) QueryAccount(ctx context.Context) (*types.Account, error) { } a := &types.Account{ - MakerCommission: fixedpoint.NewFromFloat(resp.Result.MakerFee), - TakerCommission: fixedpoint.NewFromFloat(resp.Result.TakerFee), - TotalAccountValue: fixedpoint.NewFromFloat(resp.Result.TotalAccountValue), + MakerCommission: resp.Result.MakerFee, + TakerCommission: resp.Result.TakerFee, + TotalAccountValue: resp.Result.TotalAccountValue, } balances, err := e.QueryAccountBalances(ctx) @@ -199,8 +199,8 @@ func (e *Exchange) QueryAccountBalances(ctx context.Context) (types.BalanceMap, for _, r := range resp.Result { balances[toGlobalCurrency(r.Coin)] = types.Balance{ Currency: toGlobalCurrency(r.Coin), - Available: fixedpoint.NewFromFloat(r.Free), - Locked: fixedpoint.NewFromFloat(r.Total).Sub(fixedpoint.NewFromFloat(r.Free)), + Available: r.Free, + Locked: r.Total.Sub(r.Free), } } diff --git a/pkg/exchange/ftx/rest_order_request.go b/pkg/exchange/ftx/rest_order_request.go index a8b60143c1..e55a19397f 100644 --- a/pkg/exchange/ftx/rest_order_request.go +++ b/pkg/exchange/ftx/rest_order_request.go @@ -6,6 +6,7 @@ import ( "fmt" "strconv" "time" + "github.com/c9s/bbgo/pkg/fixedpoint" ) type orderRequest struct { @@ -28,9 +29,9 @@ type orderRequest struct { type PlaceOrderPayload struct { Market string Side string - Price float64 + Price fixedpoint.Value Type string - Size float64 + Size fixedpoint.Value ReduceOnly bool IOC bool PostOnly bool diff --git a/pkg/exchange/ftx/rest_responses.go b/pkg/exchange/ftx/rest_responses.go index a086122609..cb6c318afd 100644 --- a/pkg/exchange/ftx/rest_responses.go +++ b/pkg/exchange/ftx/rest_responses.go @@ -6,6 +6,7 @@ import ( "time" "github.com/c9s/bbgo/pkg/types" + "github.com/c9s/bbgo/pkg/fixedpoint" ) // ex: 2019-03-05T09:56:55.728933+00:00 @@ -87,9 +88,9 @@ type accountResponse struct { } type account struct { - MakerFee float64 `json:"makerFee"` - TakerFee float64 `json:"takerFee"` - TotalAccountValue float64 `json:"totalAccountValue"` + MakerFee fixedpoint.Value `json:"makerFee"` + TakerFee fixedpoint.Value `json:"takerFee"` + TotalAccountValue fixedpoint.Value `json:"totalAccountValue"` } type positionsResponse struct { @@ -117,21 +118,21 @@ type positionsResponse struct { } */ type position struct { - Cost float64 `json:"cost"` - EntryPrice float64 `json:"entryPrice"` - EstimatedLiquidationPrice float64 `json:"estimatedLiquidationPrice"` + Cost fixedpoint.Value `json:"cost"` + EntryPrice fixedpoint.Value `json:"entryPrice"` + EstimatedLiquidationPrice fixedpoint.Value `json:"estimatedLiquidationPrice"` Future string `json:"future"` - InitialMarginRequirement float64 `json:"initialMarginRequirement"` - LongOrderSize float64 `json:"longOrderSize"` - MaintenanceMarginRequirement float64 `json:"maintenanceMarginRequirement"` - NetSize float64 `json:"netSize"` - OpenSize float64 `json:"openSize"` - RealizedPnl float64 `json:"realizedPnl"` - ShortOrderSize float64 `json:"shortOrderSize"` + InitialMarginRequirement fixedpoint.Value `json:"initialMarginRequirement"` + LongOrderSize fixedpoint.Value `json:"longOrderSize"` + MaintenanceMarginRequirement fixedpoint.Value `json:"maintenanceMarginRequirement"` + NetSize fixedpoint.Value `json:"netSize"` + OpenSize fixedpoint.Value `json:"openSize"` + RealizedPnl fixedpoint.Value `json:"realizedPnl"` + ShortOrderSize fixedpoint.Value `json:"shortOrderSize"` Side string `json:"Side"` - Size float64 `json:"size"` - UnrealizedPnl float64 `json:"unrealizedPnl"` - CollateralUsed float64 `json:"collateralUsed"` + Size fixedpoint.Value `json:"size"` + UnrealizedPnl fixedpoint.Value `json:"unrealizedPnl"` + CollateralUsed fixedpoint.Value `json:"collateralUsed"` } type balances struct { @@ -139,8 +140,8 @@ type balances struct { Result []struct { Coin string `json:"coin"` - Free float64 `json:"free"` - Total float64 `json:"total"` + Free fixedpoint.Value `json:"free"` + Total fixedpoint.Value `json:"total"` } `json:"result"` } @@ -180,24 +181,24 @@ type market struct { Name string `json:"name"` Enabled bool `json:"enabled"` PostOnly bool `json:"postOnly"` - PriceIncrement float64 `json:"priceIncrement"` - SizeIncrement float64 `json:"sizeIncrement"` - MinProvideSize float64 `json:"minProvideSize"` - Last float64 `json:"last"` - Bid float64 `json:"bid"` - Ask float64 `json:"ask"` - Price float64 `json:"price"` + PriceIncrement fixedpoint.Value `json:"priceIncrement"` + SizeIncrement fixedpoint.Value `json:"sizeIncrement"` + MinProvideSize fixedpoint.Value `json:"minProvideSize"` + Last fixedpoint.Value `json:"last"` + Bid fixedpoint.Value `json:"bid"` + Ask fixedpoint.Value `json:"ask"` + Price fixedpoint.Value `json:"price"` Type string `json:"type"` BaseCurrency string `json:"baseCurrency"` QuoteCurrency string `json:"quoteCurrency"` Underlying string `json:"underlying"` Restricted bool `json:"restricted"` HighLeverageFeeExempt bool `json:"highLeverageFeeExempt"` - Change1h float64 `json:"change1h"` - Change24h float64 `json:"change24h"` - ChangeBod float64 `json:"changeBod"` - QuoteVolume24h float64 `json:"quoteVolume24h"` - VolumeUsd24h float64 `json:"volumeUsd24h"` + Change1h fixedpoint.Value `json:"change1h"` + Change24h fixedpoint.Value `json:"change24h"` + ChangeBod fixedpoint.Value `json:"changeBod"` + QuoteVolume24h fixedpoint.Value `json:"quoteVolume24h"` + VolumeUsd24h fixedpoint.Value `json:"volumeUsd24h"` } /* @@ -221,12 +222,12 @@ type HistoricalPricesResponse struct { } type Candle struct { - Close float64 `json:"close"` - High float64 `json:"high"` - Low float64 `json:"low"` - Open float64 `json:"open"` + Close fixedpoint.Value `json:"close"` + High fixedpoint.Value `json:"high"` + Low fixedpoint.Value `json:"low"` + Open fixedpoint.Value `json:"open"` StartTime datetime `json:"startTime"` - Volume float64 `json:"volume"` + Volume fixedpoint.Value `json:"volume"` } type ordersHistoryResponse struct { @@ -248,16 +249,16 @@ type cancelOrderResponse struct { type order struct { CreatedAt datetime `json:"createdAt"` - FilledSize float64 `json:"filledSize"` + FilledSize fixedpoint.Value `json:"filledSize"` // Future field is not defined in the response format table but in the response example. Future string `json:"future"` ID int64 `json:"id"` Market string `json:"market"` - Price float64 `json:"price"` - AvgFillPrice float64 `json:"avgFillPrice"` - RemainingSize float64 `json:"remainingSize"` + Price fixedpoint.Value `json:"price"` + AvgFillPrice fixedpoint.Value `json:"avgFillPrice"` + RemainingSize fixedpoint.Value `json:"remainingSize"` Side string `json:"side"` - Size float64 `json:"size"` + Size fixedpoint.Value `json:"size"` Status string `json:"status"` Type string `json:"type"` ReduceOnly bool `json:"reduceOnly"` @@ -304,9 +305,9 @@ type depositHistory struct { Address address `json:"address"` Confirmations int64 `json:"confirmations"` ConfirmedTime datetime `json:"confirmedTime"` - Fee float64 `json:"fee"` + Fee fixedpoint.Value `json:"fee"` SentTime datetime `json:"sentTime"` - Size float64 `json:"size"` + Size fixedpoint.Value `json:"size"` Status string `json:"status"` Time datetime `json:"time"` Notes string `json:"notes"` @@ -360,13 +361,13 @@ type fill struct { QuoteCurrency string `json:"quoteCurrency"` Type string `json:"type"` Side types.SideType `json:"side"` - Price float64 `json:"price"` - Size float64 `json:"size"` + Price fixedpoint.Value `json:"price"` + Size fixedpoint.Value `json:"size"` OrderId uint64 `json:"orderId"` Time datetime `json:"time"` TradeId uint64 `json:"tradeId"` - FeeRate float64 `json:"feeRate"` - Fee float64 `json:"fee"` + FeeRate fixedpoint.Value `json:"feeRate"` + Fee fixedpoint.Value `json:"fee"` FeeCurrency string `json:"feeCurrency"` Liquidity string `json:"liquidity"` } @@ -379,7 +380,7 @@ type transferResponse struct { type transfer struct { Id uint `json:"id"` Coin string `json:"coin"` - Size float64 `json:"size"` + Size fixedpoint.Value `json:"size"` Time string `json:"time"` Notes string `json:"notes"` Status string `json:"status"` diff --git a/pkg/exchange/kucoin/convert.go b/pkg/exchange/kucoin/convert.go index a5f981e28e..d149547515 100644 --- a/pkg/exchange/kucoin/convert.go +++ b/pkg/exchange/kucoin/convert.go @@ -9,6 +9,7 @@ import ( "github.com/c9s/bbgo/pkg/exchange/kucoin/kucoinapi" "github.com/c9s/bbgo/pkg/types" + "github.com/c9s/bbgo/pkg/fixedpoint" ) func toGlobalBalanceMap(accounts []kucoinapi.Account) types.BalanceMap { @@ -42,28 +43,28 @@ func toGlobalMarket(m kucoinapi.Symbol) types.Market { VolumePrecision: int(math.Log10(m.BaseIncrement.Float64())), QuoteCurrency: m.QuoteCurrency, BaseCurrency: m.BaseCurrency, - MinNotional: m.QuoteMinSize.Float64(), - MinAmount: m.QuoteMinSize.Float64(), - MinQuantity: m.BaseMinSize.Float64(), - MaxQuantity: 0, // not used - StepSize: m.BaseIncrement.Float64(), - - MinPrice: 0, // not used - MaxPrice: 0, // not used - TickSize: m.PriceIncrement.Float64(), + MinNotional: m.QuoteMinSize, + MinAmount: m.QuoteMinSize, + MinQuantity: m.BaseMinSize, + MaxQuantity: fixedpoint.Zero, // not used + StepSize: m.BaseIncrement, + + MinPrice: fixedpoint.Zero, // not used + MaxPrice: fixedpoint.Zero, // not used + TickSize: m.PriceIncrement, } } func toGlobalTicker(s kucoinapi.Ticker24H) types.Ticker { return types.Ticker{ Time: s.Time.Time(), - Volume: s.Volume.Float64(), - Last: s.Last.Float64(), - Open: s.Last.Float64() - s.ChangePrice.Float64(), - High: s.High.Float64(), - Low: s.Low.Float64(), - Buy: s.Buy.Float64(), - Sell: s.Sell.Float64(), + Volume: s.Volume, + Last: s.Last, + Open: s.Last.Sub(s.ChangePrice), + High: s.High, + Low: s.Low, + Buy: s.Buy, + Sell: s.Sell, } } @@ -146,7 +147,7 @@ func toGlobalOrderStatus(o kucoinapi.Order) types.OrderStatus { var status types.OrderStatus if o.IsActive { status = types.OrderStatusNew - if o.DealSize > 0 { + if o.DealSize.Sign() > 0 { status = types.OrderStatusPartiallyFilled } } else if o.CancelExist { @@ -209,16 +210,16 @@ func toGlobalOrder(o kucoinapi.Order) types.Order { Symbol: toGlobalSymbol(o.Symbol), Side: toGlobalSide(o.Side), Type: toGlobalOrderType(o.Type), - Quantity: o.Size.Float64(), - Price: o.Price.Float64(), - StopPrice: o.StopPrice.Float64(), + Quantity: o.Size, + Price: o.Price, + StopPrice: o.StopPrice, TimeInForce: string(o.TimeInForce), }, Exchange: types.ExchangeKucoin, OrderID: hashStringID(o.ID), UUID: o.ID, Status: status, - ExecutedQuantity: o.DealSize.Float64(), + ExecutedQuantity: o.DealSize, IsWorking: o.IsActive, CreationTime: types.Time(o.CreatedAt.Time()), UpdateTime: types.Time(o.CreatedAt.Time()), // kucoin does not response updated time @@ -231,15 +232,15 @@ func toGlobalTrade(fill kucoinapi.Fill) types.Trade { ID: hashStringID(fill.TradeId), OrderID: hashStringID(fill.OrderId), Exchange: types.ExchangeKucoin, - Price: fill.Price.Float64(), - Quantity: fill.Size.Float64(), - QuoteQuantity: fill.Funds.Float64(), + Price: fill.Price, + Quantity: fill.Size, + QuoteQuantity: fill.Funds, Symbol: toGlobalSymbol(fill.Symbol), Side: toGlobalSide(string(fill.Side)), IsBuyer: fill.Side == kucoinapi.SideTypeBuy, IsMaker: fill.Liquidity == kucoinapi.LiquidityTypeMaker, Time: types.Time(fill.CreatedAt.Time()), - Fee: fill.Fee.Float64(), + Fee: fill.Fee, FeeCurrency: toGlobalSymbol(fill.FeeCurrency), } return trade diff --git a/pkg/exchange/kucoin/exchange.go b/pkg/exchange/kucoin/exchange.go index f7b7628998..7c63bd3eef 100644 --- a/pkg/exchange/kucoin/exchange.go +++ b/pkg/exchange/kucoin/exchange.go @@ -14,6 +14,7 @@ import ( "github.com/c9s/bbgo/pkg/exchange/kucoin/kucoinapi" "github.com/c9s/bbgo/pkg/types" + "github.com/c9s/bbgo/pkg/fixedpoint" ) var marketDataLimiter = rate.NewLimiter(rate.Every(500*time.Millisecond), 1) @@ -184,12 +185,12 @@ func (e *Exchange) QueryKLines(ctx context.Context, symbol string, interval type StartTime: types.Time(k.StartTime), EndTime: types.Time(k.StartTime.Add(gi.Duration() - time.Millisecond)), Interval: gi, - Open: k.Open.Float64(), - Close: k.Close.Float64(), - High: k.High.Float64(), - Low: k.Low.Float64(), - Volume: k.Volume.Float64(), - QuoteVolume: k.QuoteVolume.Float64(), + Open: k.Open, + Close: k.Close, + High: k.High, + Low: k.Low, + Volume: k.Volume, + QuoteVolume: k.QuoteVolume, Closed: true, }) } @@ -214,7 +215,8 @@ func (e *Exchange) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder if order.Market.Symbol != "" { req.Size(order.Market.FormatQuantity(order.Quantity)) } else { - req.Size(strconv.FormatFloat(order.Quantity, 'f', 8, 64)) + // TODO: report error? + req.Size(order.Quantity.FormatString(8)) } // set price field for limit orders @@ -223,7 +225,8 @@ func (e *Exchange) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder if order.Market.Symbol != "" { req.Price(order.Market.FormatPrice(order.Price)) } else { - req.Price(strconv.FormatFloat(order.Price, 'f', 8, 64)) + // TODO: report error? + req.Price(order.Price.FormatString(8)) } } @@ -248,7 +251,7 @@ func (e *Exchange) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder OrderID: hashStringID(orderResponse.OrderID), UUID: orderResponse.OrderID, Status: types.OrderStatusNew, - ExecutedQuantity: 0, + ExecutedQuantity: fixedpoint.Zero, IsWorking: true, CreationTime: types.Time(time.Now()), UpdateTime: types.Time(time.Now()), diff --git a/pkg/exchange/kucoin/stream.go b/pkg/exchange/kucoin/stream.go index c3770c8ea8..77e06a2d62 100644 --- a/pkg/exchange/kucoin/stream.go +++ b/pkg/exchange/kucoin/stream.go @@ -11,6 +11,7 @@ import ( "github.com/c9s/bbgo/pkg/exchange/kucoin/kucoinapi" "github.com/c9s/bbgo/pkg/types" "github.com/c9s/bbgo/pkg/util" + "github.com/c9s/bbgo/pkg/fixedpoint" ) const readTimeout = 30 * time.Second @@ -116,15 +117,15 @@ func (s *Stream) handlePrivateOrderEvent(e *WebSocketPrivateOrderEvent) { OrderID: hashStringID(e.OrderId), ID: hashStringID(e.TradeId), Exchange: types.ExchangeKucoin, - Price: e.MatchPrice.Float64(), - Quantity: e.MatchSize.Float64(), - QuoteQuantity: e.MatchPrice.Float64() * e.MatchSize.Float64(), + Price: e.MatchPrice, + Quantity: e.MatchSize, + QuoteQuantity: e.MatchPrice.Mul(e.MatchSize), Symbol: toGlobalSymbol(e.Symbol), Side: toGlobalSide(e.Side), IsBuyer: e.Side == "buy", IsMaker: e.Liquidity == "maker", Time: types.Time(e.Ts.Time()), - Fee: 0, // not supported + Fee: fixedpoint.Zero, // not supported FeeCurrency: "", // not supported }) } @@ -139,7 +140,7 @@ func (s *Stream) handlePrivateOrderEvent(e *WebSocketPrivateOrderEvent) { status = types.OrderStatusCanceled } } else if e.Status == "open" { - if e.FilledSize > 0 { + if e.FilledSize.Sign() > 0 { status = types.OrderStatusPartiallyFilled } } @@ -150,14 +151,14 @@ func (s *Stream) handlePrivateOrderEvent(e *WebSocketPrivateOrderEvent) { Symbol: toGlobalSymbol(e.Symbol), Side: toGlobalSide(e.Side), Type: toGlobalOrderType(e.OrderType), - Quantity: e.Size.Float64(), - Price: e.Price.Float64(), + Quantity: e.Size, + Price: e.Price, }, Exchange: types.ExchangeKucoin, OrderID: hashStringID(e.OrderId), UUID: e.OrderId, Status: status, - ExecutedQuantity: e.FilledSize.Float64(), + ExecutedQuantity: e.FilledSize, IsWorking: e.Status == "open", CreationTime: types.Time(e.OrderTime.Time()), UpdateTime: types.Time(e.Ts.Time()), diff --git a/pkg/exchange/kucoin/websocket.go b/pkg/exchange/kucoin/websocket.go index 519e49be7c..4ec493aada 100644 --- a/pkg/exchange/kucoin/websocket.go +++ b/pkg/exchange/kucoin/websocket.go @@ -6,7 +6,6 @@ import ( "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" - "github.com/c9s/bbgo/pkg/util" ) type WebSocketMessageType string @@ -97,12 +96,12 @@ type WebSocketCandleEvent struct { func (e *WebSocketCandleEvent) KLine() types.KLine { startTime := types.MustParseUnixTimestamp(e.Candles[0]) - openPrice := util.MustParseFloat(e.Candles[1]) - closePrice := util.MustParseFloat(e.Candles[2]) - highPrice := util.MustParseFloat(e.Candles[3]) - lowPrice := util.MustParseFloat(e.Candles[4]) - volume := util.MustParseFloat(e.Candles[5]) - quoteVolume := util.MustParseFloat(e.Candles[6]) + openPrice := fixedpoint.MustNewFromString(e.Candles[1]) + closePrice := fixedpoint.MustNewFromString(e.Candles[2]) + highPrice := fixedpoint.MustNewFromString(e.Candles[3]) + lowPrice := fixedpoint.MustNewFromString(e.Candles[4]) + volume := fixedpoint.MustNewFromString(e.Candles[5]) + quoteVolume := fixedpoint.MustNewFromString(e.Candles[6]) kline := types.KLine{ Exchange: types.ExchangeKucoin, Symbol: toGlobalSymbol(e.Symbol), diff --git a/pkg/exchange/max/convert.go b/pkg/exchange/max/convert.go index a242ea8516..2c1929aefc 100644 --- a/pkg/exchange/max/convert.go +++ b/pkg/exchange/max/convert.go @@ -2,7 +2,6 @@ package max import ( "fmt" - "strconv" "strings" "time" @@ -11,7 +10,6 @@ import ( "github.com/c9s/bbgo/pkg/exchange/max/maxapi" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" - "github.com/c9s/bbgo/pkg/util" ) func toGlobalCurrency(currency string) string { @@ -77,23 +75,23 @@ func toGlobalOrderStatus(orderState max.OrderState, executedVolume, remainingVol return types.OrderStatusCanceled case max.OrderStateFinalizing, max.OrderStateDone: - if executedVolume == 0 { + if executedVolume.IsZero() { return types.OrderStatusCanceled - } else if remainingVolume == 0 { + } else if remainingVolume.IsZero() { return types.OrderStatusFilled } return types.OrderStatusFilled case max.OrderStateWait: - if executedVolume > 0 && remainingVolume > 0 { + if executedVolume.Sign() > 0 && remainingVolume.Sign() > 0 { return types.OrderStatusPartiallyFilled } return types.OrderStatusNew case max.OrderStateConvert: - if executedVolume > 0 && remainingVolume > 0 { + if executedVolume.Sign() > 0 && remainingVolume.Sign() > 0 { return types.OrderStatusPartiallyFilled } @@ -189,8 +187,8 @@ func toGlobalOrder(maxOrder max.Order) (*types.Order, error) { Symbol: toGlobalSymbol(maxOrder.Market), Side: toGlobalSideType(maxOrder.Side), Type: toGlobalOrderType(maxOrder.OrderType), - Quantity: util.MustParseFloat(maxOrder.Volume), - Price: util.MustParseFloat(maxOrder.Price), + Quantity: fixedpoint.MustNewFromString(maxOrder.Volume), + Price: fixedpoint.MustNewFromString(maxOrder.Price), TimeInForce: "GTC", // MAX only supports GTC GroupID: maxOrder.GroupID, }, @@ -198,7 +196,7 @@ func toGlobalOrder(maxOrder max.Order) (*types.Order, error) { IsWorking: maxOrder.State == "wait", OrderID: maxOrder.ID, Status: toGlobalOrderStatus(maxOrder.State, executedVolume, remainingVolume), - ExecutedQuantity: executedVolume.Float64(), + ExecutedQuantity: executedVolume, CreationTime: types.Time(maxOrder.CreatedAtMs.Time()), UpdateTime: types.Time(maxOrder.CreatedAtMs.Time()), }, nil @@ -211,22 +209,22 @@ func toGlobalTrade(t max.Trade) (*types.Trade, error) { // trade time mts := time.Unix(0, t.CreatedAtMilliSeconds*int64(time.Millisecond)) - price, err := strconv.ParseFloat(t.Price, 64) + price, err := fixedpoint.NewFromString(t.Price) if err != nil { return nil, err } - quantity, err := strconv.ParseFloat(t.Volume, 64) + quantity, err := fixedpoint.NewFromString(t.Volume) if err != nil { return nil, err } - quoteQuantity, err := strconv.ParseFloat(t.Funds, 64) + quoteQuantity, err := fixedpoint.NewFromString(t.Funds) if err != nil { return nil, err } - fee, err := strconv.ParseFloat(t.Fee, 64) + fee, err := fixedpoint.NewFromString(t.Fee) if err != nil { return nil, err } @@ -276,19 +274,19 @@ func convertWebSocketTrade(t max.TradeUpdate) (*types.Trade, error) { // trade time mts := time.Unix(0, t.Timestamp*int64(time.Millisecond)) - price, err := strconv.ParseFloat(t.Price, 64) + price, err := fixedpoint.NewFromString(t.Price) if err != nil { return nil, err } - quantity, err := strconv.ParseFloat(t.Volume, 64) + quantity, err := fixedpoint.NewFromString(t.Volume) if err != nil { return nil, err } - quoteQuantity := price * quantity + quoteQuantity := price.Mul(quantity) - fee, err := strconv.ParseFloat(t.Fee, 64) + fee, err := fixedpoint.NewFromString(t.Fee) if err != nil { return nil, err } @@ -327,16 +325,16 @@ func convertWebSocketOrderUpdate(u max.OrderUpdate) (*types.Order, error) { Symbol: toGlobalSymbol(u.Market), Side: toGlobalSideType(u.Side), Type: toGlobalOrderType(u.OrderType), - Quantity: util.MustParseFloat(u.Volume), - Price: util.MustParseFloat(u.Price), - StopPrice: util.MustParseFloat(u.StopPrice), + Quantity: fixedpoint.MustNewFromString(u.Volume), + Price: fixedpoint.MustNewFromString(u.Price), + StopPrice: fixedpoint.MustNewFromString(u.StopPrice), TimeInForce: "GTC", // MAX only supports GTC GroupID: u.GroupID, }, Exchange: types.ExchangeMax, OrderID: u.ID, Status: toGlobalOrderStatus(u.State, executedVolume, remainingVolume), - ExecutedQuantity: executedVolume.Float64(), + ExecutedQuantity: executedVolume, CreationTime: types.Time(time.Unix(0, u.CreatedAtMs*int64(time.Millisecond))), }, nil } diff --git a/pkg/exchange/max/exchange.go b/pkg/exchange/max/exchange.go index 1a12ab2186..98f380bdd8 100644 --- a/pkg/exchange/max/exchange.go +++ b/pkg/exchange/max/exchange.go @@ -6,7 +6,6 @@ import ( "math" "os" "sort" - "strconv" "time" "github.com/pkg/errors" @@ -60,13 +59,13 @@ func (e *Exchange) QueryTicker(ctx context.Context, symbol string) (*types.Ticke return &types.Ticker{ Time: ticker.Time, - Volume: util.MustParseFloat(ticker.Volume), - Last: util.MustParseFloat(ticker.Last), - Open: util.MustParseFloat(ticker.Open), - High: util.MustParseFloat(ticker.High), - Low: util.MustParseFloat(ticker.Low), - Buy: util.MustParseFloat(ticker.Buy), - Sell: util.MustParseFloat(ticker.Sell), + Volume: fixedpoint.MustNewFromString(ticker.Volume), + Last: fixedpoint.MustNewFromString(ticker.Last), + Open: fixedpoint.MustNewFromString(ticker.Open), + High: fixedpoint.MustNewFromString(ticker.High), + Low: fixedpoint.MustNewFromString(ticker.Low), + Buy: fixedpoint.MustNewFromString(ticker.Buy), + Sell: fixedpoint.MustNewFromString(ticker.Sell), }, nil } @@ -102,13 +101,13 @@ func (e *Exchange) QueryTickers(ctx context.Context, symbol ...string) (map[stri } tickers[toGlobalSymbol(k)] = types.Ticker{ Time: v.Time, - Volume: util.MustParseFloat(v.Volume), - Last: util.MustParseFloat(v.Last), - Open: util.MustParseFloat(v.Open), - High: util.MustParseFloat(v.High), - Low: util.MustParseFloat(v.Low), - Buy: util.MustParseFloat(v.Buy), - Sell: util.MustParseFloat(v.Sell), + Volume: fixedpoint.MustNewFromString(v.Volume), + Last: fixedpoint.MustNewFromString(v.Last), + Open: fixedpoint.MustNewFromString(v.Open), + High: fixedpoint.MustNewFromString(v.High), + Low: fixedpoint.MustNewFromString(v.Low), + Buy: fixedpoint.MustNewFromString(v.Buy), + Sell: fixedpoint.MustNewFromString(v.Sell), } } } @@ -139,12 +138,13 @@ func (e *Exchange) QueryMarkets(ctx context.Context) (types.MarketMap, error) { MinAmount: m.MinQuoteAmount, MinQuantity: m.MinBaseAmount, - MaxQuantity: 10000.0, - StepSize: 1.0 / math.Pow10(m.BaseUnitPrecision), // make it like 0.0001 - - MinPrice: 1.0 / math.Pow10(m.QuoteUnitPrecision), // used in the price formatter - MaxPrice: 10000.0, - TickSize: 1.0 / math.Pow10(m.QuoteUnitPrecision), + MaxQuantity: fixedpoint.NewFromInt(10000), + // make it like 0.0001 + StepSize: fixedpoint.NewFromFloat(1.0 / math.Pow10(m.BaseUnitPrecision)), + // used in the price formatter + MinPrice: fixedpoint.NewFromFloat(1.0 / math.Pow10(m.QuoteUnitPrecision)), + MaxPrice: fixedpoint.NewFromInt(10000), + TickSize: fixedpoint.NewFromFloat(1.0 / math.Pow10(m.QuoteUnitPrecision)), } markets[symbol] = market @@ -421,7 +421,7 @@ func toMaxSubmitOrder(o types.SubmitOrder) (*maxapi.Order, error) { if o.Market.Symbol != "" { quantityString = o.Market.FormatQuantity(o.Quantity) } else { - quantityString = strconv.FormatFloat(o.Quantity, 'f', -1, 64) + quantityString = o.Quantity.String() } maxOrder := maxapi.Order{ @@ -447,7 +447,7 @@ func toMaxSubmitOrder(o types.SubmitOrder) (*maxapi.Order, error) { if o.Market.Symbol != "" { priceInString = o.Market.FormatPrice(o.Price) } else { - priceInString = strconv.FormatFloat(o.Price, 'f', -1, 64) + priceInString = o.Price.String() } maxOrder.Price = priceInString } @@ -459,7 +459,7 @@ func toMaxSubmitOrder(o types.SubmitOrder) (*maxapi.Order, error) { if o.Market.Symbol != "" { priceInString = o.Market.FormatPrice(o.StopPrice) } else { - priceInString = strconv.FormatFloat(o.StopPrice, 'f', -1, 64) + priceInString = o.StopPrice.String() } maxOrder.StopPrice = priceInString } @@ -713,11 +713,11 @@ func (e *Exchange) QueryWithdrawHistory(ctx context.Context, asset string, since Exchange: types.ExchangeMax, ApplyTime: types.Time(time.Unix(d.CreatedAt, 0)), Asset: toGlobalCurrency(d.Currency), - Amount: util.MustParseFloat(d.Amount), + Amount: fixedpoint.MustNewFromString(d.Amount), Address: "", AddressTag: "", TransactionID: d.TxID, - TransactionFee: util.MustParseFloat(d.Fee), + TransactionFee: fixedpoint.MustNewFromString(d.Fee), TransactionFeeCurrency: d.FeeCurrency, // WithdrawOrderID: d.WithdrawOrderID, // Network: d.Network, @@ -784,7 +784,7 @@ func (e *Exchange) QueryDepositHistory(ctx context.Context, asset string, since, allDeposits = append(allDeposits, types.Deposit{ Exchange: types.ExchangeMax, Time: types.Time(time.Unix(d.CreatedAt, 0)), - Amount: util.MustParseFloat(d.Amount), + Amount: fixedpoint.MustNewFromString(d.Amount), Asset: toGlobalCurrency(d.Currency), Address: "", // not supported AddressTag: "", // not supported @@ -965,11 +965,14 @@ func (e *Exchange) QueryKLines(ctx context.Context, symbol string, interval type return kLines, nil } -func (e *Exchange) QueryAveragePrice(ctx context.Context, symbol string) (float64, error) { +var Two = fixedpoint.NewFromInt(2) + +func (e *Exchange) QueryAveragePrice(ctx context.Context, symbol string) (fixedpoint.Value, error) { ticker, err := e.client.PublicService.Ticker(toLocalSymbol(symbol)) if err != nil { - return 0, err + return fixedpoint.Zero, err } - return (util.MustParseFloat(ticker.Sell) + util.MustParseFloat(ticker.Buy)) / 2, nil + return fixedpoint.MustNewFromString(ticker.Sell). + Add(fixedpoint.MustNewFromString(ticker.Buy)).Div(Two), nil } diff --git a/pkg/exchange/max/maxapi/public.go b/pkg/exchange/max/maxapi/public.go index a26931b4fa..cb0859667e 100644 --- a/pkg/exchange/max/maxapi/public.go +++ b/pkg/exchange/max/maxapi/public.go @@ -11,6 +11,7 @@ import ( "github.com/pkg/errors" "github.com/valyala/fastjson" + "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" ) @@ -25,8 +26,8 @@ type Market struct { BaseUnitPrecision int `json:"base_unit_precision"` QuoteUnit string `json:"quote_unit"` QuoteUnitPrecision int `json:"quote_unit_precision"` - MinBaseAmount float64 `json:"min_base_amount"` - MinQuoteAmount float64 `json:"min_quote_amount"` + MinBaseAmount fixedpoint.Value `json:"min_base_amount"` + MinQuoteAmount fixedpoint.Value `json:"min_quote_amount"` } type Ticker struct { @@ -206,8 +207,8 @@ type KLine struct { Symbol string Interval string StartTime, EndTime time.Time - Open, High, Low, Close float64 - Volume float64 + Open, High, Low, Close fixedpoint.Value + Volume fixedpoint.Value Closed bool } @@ -309,11 +310,11 @@ func parseKLines(payload []byte, symbol, resolution string, interval Interval) ( Interval: resolution, StartTime: startTime, EndTime: endTime, - Open: slice[1].GetFloat64(), - High: slice[2].GetFloat64(), - Low: slice[3].GetFloat64(), - Close: slice[4].GetFloat64(), - Volume: slice[5].GetFloat64(), + Open: fixedpoint.MustNewFromBytes(slice[1].GetStringBytes()), + High: fixedpoint.MustNewFromBytes(slice[2].GetStringBytes()), + Low: fixedpoint.MustNewFromBytes(slice[3].GetStringBytes()), + Close: fixedpoint.MustNewFromBytes(slice[4].GetStringBytes()), + Volume: fixedpoint.MustNewFromBytes(slice[5].GetStringBytes()), Closed: isClosed, }) } diff --git a/pkg/exchange/max/maxapi/public_parser.go b/pkg/exchange/max/maxapi/public_parser.go index 167470933e..66f0cf88a5 100644 --- a/pkg/exchange/max/maxapi/public_parser.go +++ b/pkg/exchange/max/maxapi/public_parser.go @@ -9,7 +9,6 @@ import ( "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" - "github.com/c9s/bbgo/pkg/util" ) var ErrIncorrectBookEntryElementLength = errors.New("incorrect book entry element length") @@ -122,12 +121,12 @@ func (k KLinePayload) KLine() types.KLine { EndTime: types.Time(time.Unix(0, k.EndTime*int64(time.Millisecond))), Symbol: k.Market, Interval: types.Interval(k.Resolution), - Open: util.MustParseFloat(k.Open), - Close: util.MustParseFloat(k.Close), - High: util.MustParseFloat(k.High), - Low: util.MustParseFloat(k.Low), - Volume: util.MustParseFloat(k.Volume), - QuoteVolume: 0, // TODO: add this from kingfisher + Open: fixedpoint.MustNewFromString(k.Open), + Close: fixedpoint.MustNewFromString(k.Close), + High: fixedpoint.MustNewFromString(k.High), + Low: fixedpoint.MustNewFromString(k.Low), + Volume: fixedpoint.MustNewFromString(k.Volume), + QuoteVolume: fixedpoint.Zero, // TODO: add this from kingfisher LastTradeID: uint64(k.LastTradeID), NumberOfTrades: 0, // TODO: add this from kingfisher Closed: k.Closed, @@ -211,11 +210,11 @@ func parseKLineEvent(val *fastjson.Value) (*KLineEvent, error) { Interval: string(val.GetStringBytes("k", "R")), StartTime: time.Unix(0, val.GetInt64("k", "ST")*int64(time.Millisecond)), EndTime: time.Unix(0, val.GetInt64("k", "ET")*int64(time.Millisecond)), - Open: util.MustParseFloat(string(val.GetStringBytes("k", "O"))), - High: util.MustParseFloat(string(val.GetStringBytes("k", "H"))), - Low: util.MustParseFloat(string(val.GetStringBytes("k", "L"))), - Close: util.MustParseFloat(string(val.GetStringBytes("k", "C"))), - Volume: util.MustParseFloat(string(val.GetStringBytes("k", "v"))), + Open: fixedpoint.MustNewFromBytes(val.GetStringBytes("k", "O")), + High: fixedpoint.MustNewFromBytes(val.GetStringBytes("k", "H")), + Low: fixedpoint.MustNewFromBytes(val.GetStringBytes("k", "L")), + Close: fixedpoint.MustNewFromBytes(val.GetStringBytes("k", "C")), + Volume: fixedpoint.MustNewFromBytes(val.GetStringBytes("k", "v")), Closed: val.GetBool("k", "x"), } diff --git a/pkg/exchange/okex/convert.go b/pkg/exchange/okex/convert.go index 35b530fe66..840b4e4c2c 100644 --- a/pkg/exchange/okex/convert.go +++ b/pkg/exchange/okex/convert.go @@ -7,6 +7,7 @@ import ( "github.com/c9s/bbgo/pkg/exchange/okex/okexapi" "github.com/c9s/bbgo/pkg/types" + "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/pkg/errors" ) @@ -28,13 +29,13 @@ func toLocalSymbol(symbol string) string { func toGlobalTicker(marketTicker okexapi.MarketTicker) *types.Ticker { return &types.Ticker{ Time: marketTicker.Timestamp.Time(), - Volume: marketTicker.Volume24H.Float64(), - Last: marketTicker.Last.Float64(), - Open: marketTicker.Open24H.Float64(), - High: marketTicker.High24H.Float64(), - Low: marketTicker.Low24H.Float64(), - Buy: marketTicker.BidPrice.Float64(), - Sell: marketTicker.AskPrice.Float64(), + Volume: marketTicker.Volume24H, + Last: marketTicker.Last, + Open: marketTicker.Open24H, + High: marketTicker.High24H, + Low: marketTicker.Low24H, + Buy: marketTicker.BidPrice, + Sell: marketTicker.AskPrice, } } @@ -139,15 +140,15 @@ func toGlobalTrades(orderDetails []okexapi.OrderDetails) ([]types.Trade, error) ID: uint64(tradeID), OrderID: uint64(orderID), Exchange: types.ExchangeOKEx, - Price: orderDetail.LastFilledPrice.Float64(), - Quantity: orderDetail.LastFilledQuantity.Float64(), - QuoteQuantity: orderDetail.LastFilledPrice.Float64() * orderDetail.LastFilledQuantity.Float64(), + Price: orderDetail.LastFilledPrice, + Quantity: orderDetail.LastFilledQuantity, + QuoteQuantity: orderDetail.LastFilledPrice.Mul(orderDetail.LastFilledQuantity), Symbol: toGlobalSymbol(orderDetail.InstrumentID), Side: side, IsBuyer: side == types.SideTypeBuy, IsMaker: orderDetail.ExecutionType == "M", Time: types.Time(orderDetail.LastFilledTime), - Fee: orderDetail.LastFilledFee.Float64(), + Fee: orderDetail.LastFilledFee, FeeCurrency: orderDetail.LastFilledFeeCurrency, IsMargin: false, IsIsolated: false, @@ -199,15 +200,15 @@ func toGlobalOrders(orderDetails []okexapi.OrderDetails) ([]types.Order, error) Symbol: toGlobalSymbol(orderDetail.InstrumentID), Side: side, Type: orderType, - Price: orderDetail.Price.Float64(), - Quantity: orderDetail.Quantity.Float64(), - StopPrice: 0, // not supported yet + Price: orderDetail.Price, + Quantity: orderDetail.Quantity, + StopPrice: fixedpoint.Zero, // not supported yet TimeInForce: timeInForce, }, Exchange: types.ExchangeOKEx, OrderID: uint64(orderID), Status: orderStatus, - ExecutedQuantity: orderDetail.FilledQuantity.Float64(), + ExecutedQuantity: orderDetail.FilledQuantity, IsWorking: isWorking, CreationTime: types.Time(orderDetail.CreationTime), UpdateTime: types.Time(orderDetail.UpdateTime), diff --git a/pkg/exchange/okex/exchange.go b/pkg/exchange/okex/exchange.go index 2368fd812a..4a80f5826f 100644 --- a/pkg/exchange/okex/exchange.go +++ b/pkg/exchange/okex/exchange.go @@ -11,6 +11,7 @@ import ( "github.com/c9s/bbgo/pkg/exchange/okex/okexapi" "github.com/c9s/bbgo/pkg/types" + "github.com/c9s/bbgo/pkg/fixedpoint" ) // OKB is the platform currency of OKEx, pre-allocate static string here @@ -69,17 +70,17 @@ func (e *Exchange) QueryMarkets(ctx context.Context) (types.MarketMap, error) { VolumePrecision: int(-math.Log10(instrument.LotSize.Float64())), // TickSize: OKEx's price tick, for BTC-USDT it's "0.1" - TickSize: instrument.TickSize.Float64(), + TickSize: instrument.TickSize, // Quantity step size, for BTC-USDT, it's "0.00000001" - StepSize: instrument.LotSize.Float64(), + StepSize: instrument.LotSize, // for BTC-USDT, it's "0.00001" - MinQuantity: instrument.MinSize.Float64(), + MinQuantity: instrument.MinSize, // OKEx does not offer minimal notional, use 1 USD here. - MinNotional: 1.0, - MinAmount: 1.0, + MinNotional: fixedpoint.One, + MinAmount: fixedpoint.One, } markets[symbol] = market } @@ -170,7 +171,8 @@ func (e *Exchange) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder if order.Market.Symbol != "" { orderReq.Quantity(order.Market.FormatQuantity(order.Quantity)) } else { - orderReq.Quantity(strconv.FormatFloat(order.Quantity, 'f', 8, 64)) + // TODO report error + orderReq.Quantity(order.Quantity.FormatString(8)) } // set price field for limit orders @@ -179,7 +181,8 @@ func (e *Exchange) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder if order.Market.Symbol != "" { orderReq.Price(order.Market.FormatPrice(order.Price)) } else { - orderReq.Price(strconv.FormatFloat(order.Price, 'f', 8, 64)) + // TODO report error + orderReq.Price(order.Price.FormatString(8)) } } @@ -214,7 +217,7 @@ func (e *Exchange) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder Exchange: types.ExchangeOKEx, OrderID: uint64(orderID), Status: types.OrderStatusNew, - ExecutedQuantity: 0, + ExecutedQuantity: fixedpoint.Zero, IsWorking: true, CreationTime: types.Time(time.Now()), UpdateTime: types.Time(time.Now()), diff --git a/pkg/fixedpoint/dec.go b/pkg/fixedpoint/dec.go index e01de1c174..f4f1ba43a4 100644 --- a/pkg/fixedpoint/dec.go +++ b/pkg/fixedpoint/dec.go @@ -108,6 +108,13 @@ var halfpow10 = [...]uint64{ 500000000000000000, 5000000000000000000} +func min(a int, b int) int { + if a < b { + return a + } + return b +} + func (v Value) Value() (driver.Value, error) { return v.Float64(), nil } @@ -233,6 +240,41 @@ func Inf(sign int8) Value { } } +func (dn Value) FormatString(prec int) 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[:min(prec, nd)] + } else if -nd < e && e <= -1 { + // decimal within + dec := nd + e + return sign + digits[:dec] + "." + digits[dec:min(dec+prec, nd)] + } 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:min(1+prec, nd)] + } + return sign + digits[:1] + after + "e" + strconv.Itoa(int(dn.exp-1)) + } +} + // String returns a string representation of the Value func (dn Value) String() string { if dn.sign == 0 { @@ -360,12 +402,12 @@ func NewFromString(s string) (Value, error) { s = s[:length-1] } r := &reader{s, 0} - sign := getSign(r) + sign := r.getSign() if r.matchStr("inf") { return Inf(sign), nil } - coef, exp := getCoef(r) - exp += getExp(r) + coef, exp := r.getCoef() + exp += r.getExp() if r.len() != 0 { // didn't consume entire string return Zero, errors.New("invalid number") } else if coef == 0 || exp < math.MinInt8 { @@ -388,6 +430,158 @@ func MustNewFromString(input string) Value { return v } +func NewFromBytes(s []byte) (Value, error) { + length := len(s) + isPercentage := s[length - 1] == '%' + if isPercentage { + s = s[:length-1] + } + r := &readerBytes{s, 0} + sign := r.getSign() + if r.matchStr("inf") { + return Inf(sign), nil + } + coef, exp := r.getCoef() + exp += r.getExp() + 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 MustNewFromBytes(input []byte) Value { + v, err := NewFromBytes(input) + if err != nil { + panic(fmt.Errorf("cannot parse %s into fixedpoint, error: %s", input, err.Error())) + } + return v +} + + +// TODO: refactor by interface + +type readerBytes struct { + s []byte + i int +} + +func (r *readerBytes) cur() byte { + if r.i >= len(r.s) { + return 0 + } + return byte(r.s[r.i]) +} + +func (r *readerBytes) prev() byte { + if r.i == 0 { + return 0 + } + return byte(r.s[r.i-1]) +} + +func (r *readerBytes) len() int { + return len(r.s) - r.i +} + +func (r *readerBytes) match(c byte) bool { + if r.cur() == c { + r.i++ + return true + } + return false +} + +func (r *readerBytes) matchDigit() bool { + c := r.cur() + if '0' <= c && c <= '9' { + r.i++ + return true + } + return false +} + +func (r *readerBytes) matchStr(pre string) bool { + for i, c := range r.s[r.i:] { + if pre[i] != c { + return false + } + } + r.i += len(pre) + return true +} + +func (r *readerBytes) getSign() int8 { + if r.match('-') { + return int8(signNeg) + } + r.match('+') + return int8(signPos) +} + +func (r *readerBytes) getCoef() (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 (r *readerBytes) getExp() int { + e := 0 + if r.match('e') || r.match('E') { + esign := r.getSign() + for r.matchDigit() { + e = e*10 + int(r.prev()-'0') + } + e *= int(esign) + } + return e +} + type reader struct { s string i int @@ -436,7 +630,7 @@ func (r *reader) matchStr(pre string) bool { return false } -func getSign(r *reader) int8 { +func (r *reader) getSign() int8 { if r.match('-') { return int8(signNeg) } @@ -444,7 +638,7 @@ func getSign(r *reader) int8 { return int8(signPos) } -func getCoef(r *reader) (uint64, int) { +func (r *reader) getCoef() (uint64, int) { digits := false beforeDecimal := true for r.match('0') { @@ -488,10 +682,10 @@ func getCoef(r *reader) (uint64, int) { return n, exp } -func getExp(r *reader) int { +func (r *reader) getExp() int { e := 0 if r.match('e') || r.match('E') { - esign := getSign(r) + esign := r.getSign() for r.matchDigit() { e = e*10 + int(r.prev()-'0') } diff --git a/pkg/fixedpoint/dec_test.go b/pkg/fixedpoint/dec_test.go index 9b20d31818..37ce50eb71 100644 --- a/pkg/fixedpoint/dec_test.go +++ b/pkg/fixedpoint/dec_test.go @@ -48,6 +48,7 @@ func TestMulString(t *testing.T) { y := NewFromFloat(10.55) x = x.Mul(y) assert.Equal(t, "111.3025", x.String()) + assert.Equal(t, "111.30", x.FormatString(2)) } // Not used diff --git a/pkg/indicator/ad.go b/pkg/indicator/ad.go index 8b201044a5..ad2ba859fd 100644 --- a/pkg/indicator/ad.go +++ b/pkg/indicator/ad.go @@ -23,10 +23,10 @@ type AD struct { } func (inc *AD) update(kLine types.KLine) { - close := kLine.Close - high := kLine.High - low := kLine.Low - volume := kLine.Volume + close := kLine.Close.Float64() + high := kLine.High.Float64() + low := kLine.Low.Float64() + volume := kLine.Volume.Float64() moneyFlowVolume := ((2*close - high - low) / (high - low)) * volume diff --git a/pkg/indicator/boll.go b/pkg/indicator/boll.go index 0b2575eb18..dd68abb2ec 100644 --- a/pkg/indicator/boll.go +++ b/pkg/indicator/boll.go @@ -93,7 +93,7 @@ func (inc *BOLL) calculateAndUpdate(kLines []types.KLine) { var prices []float64 for _, k := range recentK { - prices = append(prices, k.Close) + prices = append(prices, k.Close.Float64()) } var std = stat.StdDev(prices, nil) diff --git a/pkg/indicator/ewma.go b/pkg/indicator/ewma.go index f8e2fcb8d6..bd100fb89c 100644 --- a/pkg/indicator/ewma.go +++ b/pkg/indicator/ewma.go @@ -115,15 +115,15 @@ func ewma(prices []float64, multiplier float64) float64 { type KLinePriceMapper func(k types.KLine) float64 func KLineOpenPriceMapper(k types.KLine) float64 { - return k.Open + return k.Open.Float64() } func KLineClosePriceMapper(k types.KLine) float64 { - return k.Close + return k.Close.Float64() } func KLineTypicalPriceMapper(k types.KLine) float64 { - return (k.High + k.Low + k.Close) / float64(3) + return (k.High.Float64() + k.Low.Float64() + k.Close.Float64()) / 3. } func MapKLinePrice(kLines []types.KLine, f KLinePriceMapper) (prices []float64) { diff --git a/pkg/indicator/obv.go b/pkg/indicator/obv.go index 0e2956b7f9..4ed3d7d716 100644 --- a/pkg/indicator/obv.go +++ b/pkg/indicator/obv.go @@ -4,7 +4,6 @@ import ( "time" "github.com/c9s/bbgo/pkg/types" - "github.com/c9s/bbgo/pkg/fixedpoint" ) /* @@ -17,15 +16,15 @@ On-Balance Volume (OBV) Definition type OBV struct { types.IntervalWindow Values types.Float64Slice - PrePrice fixedpoint.Value + PrePrice float64 EndTime time.Time - UpdateCallbacks []func(value fixedpoint.Value) + UpdateCallbacks []func(value float64) } func (inc *OBV) update(kLine types.KLine, priceF KLinePriceMapper) { price := priceF(kLine) - volume := kLine.Volume + volume := kLine.Volume.Float64() if len(inc.Values) == 0 { inc.PrePrice = price diff --git a/pkg/indicator/stoch.go b/pkg/indicator/stoch.go index a99e450259..0d088f2750 100644 --- a/pkg/indicator/stoch.go +++ b/pkg/indicator/stoch.go @@ -30,10 +30,11 @@ func (inc *STOCH) update(kLine types.KLine) { inc.KLineWindow.Add(kLine) inc.KLineWindow.Truncate(inc.Window) - lowest := inc.KLineWindow.GetLow() - highest := inc.KLineWindow.GetHigh() + lowest := inc.KLineWindow.GetLow().Float64() + highest := inc.KLineWindow.GetHigh().Float64() + clos := kLine.Close.Float64() - k := 100.0 * (kLine.Close - lowest) / (highest - lowest) + k := 100.0 * (clos - lowest) / (highest - lowest) inc.K.Push(k) d := inc.K.Tail(DPeriod).Mean() diff --git a/pkg/indicator/vwap.go b/pkg/indicator/vwap.go index 1f32efcb7e..fbdb1de92a 100644 --- a/pkg/indicator/vwap.go +++ b/pkg/indicator/vwap.go @@ -44,7 +44,7 @@ func (inc *VWAP) calculateVWAP(kLines []types.KLine, priceF KLinePriceMapper) (v func (inc *VWAP) update(kLine types.KLine, priceF KLinePriceMapper, multiplier float64) { // multiplier = 1 or -1 price := priceF(kLine) - volume := kLine.Volume + volume := kLine.Volume.Float64() inc.WeightedSum += multiplier * price * volume inc.VolumeSum += multiplier * volume @@ -63,7 +63,7 @@ func (inc *VWAP) calculateAndUpdate(kLines []types.KLine) { if len(inc.Values) == 0 { // for the first value, we should use the close price price := priceF(kLines[0]) - volume := kLines[0].Volume + volume := kLines[0].Volume.Float64() inc.Values = []float64{price} inc.WeightedSum = price * volume diff --git a/pkg/indicator/vwma.go b/pkg/indicator/vwma.go index 0298c547c9..643fa7767f 100644 --- a/pkg/indicator/vwma.go +++ b/pkg/indicator/vwma.go @@ -35,11 +35,11 @@ func (inc *VWMA) Last() float64 { } func KLinePriceVolumeMapper(k types.KLine) float64 { - return k.Close * k.Volume + return k.Close.Mul(k.Volume).Float64() } func KLineVolumeMapper(k types.KLine) float64 { - return k.Volume + return k.Volume.Float64() } func (inc *VWMA) calculateAndUpdate(kLines []types.KLine) { diff --git a/pkg/service/order.go b/pkg/service/order.go index 9b84989743..a32c368a17 100644 --- a/pkg/service/order.go +++ b/pkg/service/order.go @@ -81,7 +81,7 @@ func (s *OrderService) Sync(ctx context.Context, exchange types.Exchange, symbol } // skip canceled and not filled orders - if order.Status == types.OrderStatusCanceled && order.ExecutedQuantity == 0.0 { + if order.Status == types.OrderStatusCanceled && order.ExecutedQuantity.IsZero() { continue } diff --git a/pkg/types/deposit.go b/pkg/types/deposit.go index 615ff90f1e..2735a4445d 100644 --- a/pkg/types/deposit.go +++ b/pkg/types/deposit.go @@ -2,6 +2,7 @@ package types import ( "time" + "github.com/c9s/bbgo/pkg/fixedpoint" ) type DepositStatus string @@ -25,7 +26,7 @@ type Deposit struct { GID int64 `json:"gid" db:"gid"` Exchange ExchangeName `json:"exchange" db:"exchange"` Time Time `json:"time" db:"time"` - Amount float64 `json:"amount" db:"amount"` + Amount fixedpoint.Value `json:"amount" db:"amount"` Asset string `json:"asset" db:"asset"` Address string `json:"address" db:"address"` AddressTag string `json:"addressTag"` diff --git a/pkg/types/market.go b/pkg/types/market.go index de2039f713..51f7fa9edc 100644 --- a/pkg/types/market.go +++ b/pkg/types/market.go @@ -74,30 +74,30 @@ type Market struct { // The MIN_NOTIONAL filter defines the minimum notional value allowed for an order on a symbol. // An order's notional value is the price * quantity - MinNotional float64 `json:"minNotional,omitempty"` - MinAmount float64 `json:"minAmount,omitempty"` + MinNotional fixedpoint.Value `json:"minNotional,omitempty"` + MinAmount fixedpoint.Value `json:"minAmount,omitempty"` // The LOT_SIZE filter defines the quantity - MinQuantity float64 `json:"minQuantity,omitempty"` + MinQuantity fixedpoint.Value `json:"minQuantity,omitempty"` // MaxQuantity is currently not used in the code - MaxQuantity float64 `json:"maxQuantity,omitempty"` + MaxQuantity fixedpoint.Value `json:"maxQuantity,omitempty"` // StepSize is the step size of quantity // can be converted from precision, e.g. // 1.0 / math.Pow10(m.BaseUnitPrecision) - StepSize float64 `json:"stepSize,omitempty"` + StepSize fixedpoint.Value `json:"stepSize,omitempty"` - MinPrice float64 `json:"minPrice,omitempty"` - MaxPrice float64 `json:"maxPrice,omitempty"` + MinPrice fixedpoint.Value `json:"minPrice,omitempty"` + MaxPrice fixedpoint.Value `json:"maxPrice,omitempty"` // TickSize is the step size of price - TickSize float64 `json:"tickSize,omitempty"` + TickSize fixedpoint.Value `json:"tickSize,omitempty"` } // TruncateQuantity uses the step size to truncate floating number, in order to avoid the rounding issue func (m Market) TruncateQuantity(quantity fixedpoint.Value) fixedpoint.Value { - stepRound := math.Pow10(-int(math.Log10(m.StepSize))) + stepRound := math.Pow10(-int(math.Log10(m.StepSize.Float64()))) return fixedpoint.NewFromFloat(math.Trunc(quantity.Float64()*stepRound) / stepRound) } @@ -125,55 +125,59 @@ func (m Market) QuoteCurrencyFormatter() *accounting.Accounting { return a } -func (m Market) FormatPriceCurrency(val float64) string { +func (m Market) FormatPriceCurrency(val fixedpoint.Value) string { switch m.QuoteCurrency { case "USD", "USDT": - return USD.FormatMoneyFloat64(val) + return USD.FormatMoney(val) case "BTC": - return BTC.FormatMoneyFloat64(val) + return BTC.FormatMoney(val) case "BNB": - return BNB.FormatMoneyFloat64(val) + return BNB.FormatMoney(val) } return m.FormatPrice(val) } -func (m Market) FormatPrice(val float64) string { +func (m Market) FormatPrice(val fixedpoint.Value) string { // p := math.Pow10(m.PricePrecision) return formatPrice(val, m.TickSize) } -func formatPrice(price float64, tickSize float64) string { - prec := int(math.Round(math.Abs(math.Log10(tickSize)))) +func formatPrice(price fixedpoint.Value, tickSize fixedpoint.Value) string { + // TODO Round + prec := int(math.Round(math.Abs(math.Log10(tickSize.Float64())))) p := math.Pow10(prec) - price = math.Trunc(price*p) / p - return strconv.FormatFloat(price, 'f', prec, 64) + pp := math.Trunc(price.Float64()*p) / p + return strconv.FormatFloat(pp, 'f', prec, 64) } -func (m Market) FormatQuantity(val float64) string { +func (m Market) FormatQuantity(val fixedpoint.Value) string { return formatQuantity(val, m.StepSize) } -func formatQuantity(quantity float64, lot float64) string { - prec := int(math.Round(math.Abs(math.Log10(lot)))) +func formatQuantity(quantity fixedpoint.Value, lot fixedpoint.Value) string { + // TODO Round + prec := int(math.Round(math.Abs(math.Log10(lot.Float64())))) p := math.Pow10(prec) - quantity = math.Trunc(quantity*p) / p - return strconv.FormatFloat(quantity, 'f', prec, 64) + q := math.Trunc(quantity.Float64() * p) / p + return strconv.FormatFloat(q, 'f', prec, 64) } -func (m Market) FormatVolume(val float64) string { +func (m Market) FormatVolume(val fixedpoint.Value) string { + // TODO Round p := math.Pow10(m.VolumePrecision) - val = math.Trunc(val*p) / p - return strconv.FormatFloat(val, 'f', m.VolumePrecision, 64) + v := math.Trunc(val.Float64()*p) / p + return strconv.FormatFloat(v, 'f', m.VolumePrecision, 64) } -func (m Market) CanonicalizeVolume(val float64) float64 { +func (m Market) CanonicalizeVolume(val fixedpoint.Value) float64 { + // TODO Round p := math.Pow10(m.VolumePrecision) - return math.Trunc(p*val) / p + return math.Trunc(p*val.Float64()) / p } type MarketMap map[string]Market diff --git a/pkg/types/order.go b/pkg/types/order.go index 3e58cb3471..314c98e55b 100644 --- a/pkg/types/order.go +++ b/pkg/types/order.go @@ -11,6 +11,7 @@ import ( "github.com/slack-go/slack" "github.com/c9s/bbgo/pkg/util" + "github.com/c9s/bbgo/pkg/fixedpoint" ) func init() { @@ -108,9 +109,9 @@ type SubmitOrder struct { Side SideType `json:"side" db:"side"` Type OrderType `json:"orderType" db:"order_type"` - Quantity float64 `json:"quantity" db:"quantity"` - Price float64 `json:"price" db:"price"` - StopPrice float64 `json:"stopPrice,omitempty" db:"stop_price"` + Quantity fixedpoint.Value `json:"quantity" db:"quantity"` + Price fixedpoint.Value `json:"price" db:"price"` + StopPrice fixedpoint.Value `json:"stopPrice,omitempty" db:"stop_price"` Market Market `json:"-" db:"-"` @@ -129,40 +130,40 @@ type SubmitOrder struct { func (o SubmitOrder) String() string { switch o.Type { case OrderTypeMarket: - return fmt.Sprintf("SubmitOrder %s %s %s %f", o.Symbol, o.Type, o.Side, o.Quantity) + return fmt.Sprintf("SubmitOrder %s %s %s %s", o.Symbol, o.Type, o.Side, o.Quantity.String()) } - return fmt.Sprintf("SubmitOrder %s %s %s %f @ %f", o.Symbol, o.Type, o.Side, o.Quantity, o.Price) + return fmt.Sprintf("SubmitOrder %s %s %s %s @ %s", o.Symbol, o.Type, o.Side, o.Quantity.String(), o.Price.String()) } func (o SubmitOrder) PlainText() string { switch o.Type { case OrderTypeMarket: - return fmt.Sprintf("SubmitOrder %s %s %s %f", o.Symbol, o.Type, o.Side, o.Quantity) + return fmt.Sprintf("SubmitOrder %s %s %s %s", o.Symbol, o.Type, o.Side, o.Quantity.String()) } - return fmt.Sprintf("SubmitOrder %s %s %s %f @ %f", o.Symbol, o.Type, o.Side, o.Quantity, o.Price) + return fmt.Sprintf("SubmitOrder %s %s %s %s @ %s", o.Symbol, o.Type, o.Side, o.Quantity.String(), o.Price.String()) } func (o SubmitOrder) SlackAttachment() slack.Attachment { var fields = []slack.AttachmentField{ {Title: "Symbol", Value: o.Symbol, Short: true}, {Title: "Side", Value: string(o.Side), Short: true}, - {Title: "Price", Value: trimTrailingZeroFloat(o.Price), Short: true}, - {Title: "Quantity", Value: trimTrailingZeroFloat(o.Quantity), Short: true}, + {Title: "Price", Value: o.Price.String(), Short: true}, + {Title: "Quantity", Value: o.Quantity.String(), Short: true}, } - if o.Price > 0 && o.Quantity > 0 && len(o.Market.QuoteCurrency) > 0 { + if o.Price.Sign() > 0 && o.Quantity.Sign() > 0 && len(o.Market.QuoteCurrency) > 0 { if IsFiatCurrency(o.Market.QuoteCurrency) { fields = append(fields, slack.AttachmentField{ Title: "Amount", - Value: USD.FormatMoneyFloat64(o.Price * o.Quantity), + Value: USD.FormatMoney(o.Price.Mul(o.Quantity)), Short: true, }) } else { fields = append(fields, slack.AttachmentField{ Title: "Amount", - Value: fmt.Sprintf("%f %s", o.Price*o.Quantity, o.Market.QuoteCurrency), + Value: fmt.Sprintf("%s %s", o.Price.Mul(o.Quantity).String(), o.Market.QuoteCurrency), Short: true, }) } @@ -201,7 +202,7 @@ type Order struct { UUID string `json:"uuid,omitempty"` Status OrderStatus `json:"status" db:"status"` - ExecutedQuantity float64 `json:"executedQuantity" db:"executed_quantity"` + ExecutedQuantity fixedpoint.Value `json:"executedQuantity" db:"executed_quantity"` IsWorking bool `json:"isWorking" db:"is_working"` CreationTime Time `json:"creationTime" db:"created_at"` UpdateTime Time `json:"updateTime" db:"updated_at"` @@ -214,7 +215,7 @@ type Order struct { // so that we can post the order later when we want to restore the orders. func (o Order) Backup() SubmitOrder { so := o.SubmitOrder - so.Quantity = o.Quantity - o.ExecutedQuantity + so.Quantity = o.Quantity.Sub(o.ExecutedQuantity) // ClientOrderID can not be reused so.ClientOrderID = "" @@ -229,14 +230,14 @@ func (o Order) String() string { orderID = strconv.FormatUint(o.OrderID, 10) } - return fmt.Sprintf("ORDER %s %s %s %s %f/%f @ %f -> %s", + return fmt.Sprintf("ORDER %s %s %s %s %s/%s @ %s -> %s", o.Exchange.String(), orderID, o.Symbol, o.Side, - o.ExecutedQuantity, - o.Quantity, - o.Price, + o.ExecutedQuantity.String(), + o.Quantity.String(), + o.Price.String(), o.Status) } @@ -247,9 +248,9 @@ func (o Order) PlainText() string { o.Symbol, o.Type, o.Side, - util.FormatFloat(o.Price, 2), - util.FormatFloat(o.ExecutedQuantity, 2), - util.FormatFloat(o.Quantity, 4), + o.Price.FormatString(2), + o.ExecutedQuantity.FormatString(2), + o.Quantity.FormatString(4), o.Status) } @@ -257,10 +258,10 @@ func (o Order) SlackAttachment() slack.Attachment { var fields = []slack.AttachmentField{ {Title: "Symbol", Value: o.Symbol, Short: true}, {Title: "Side", Value: string(o.Side), Short: true}, - {Title: "Price", Value: trimTrailingZeroFloat(o.Price), Short: true}, + {Title: "Price", Value: o.Price.String(), Short: true}, { Title: "Executed Quantity", - Value: trimTrailingZeroFloat(o.ExecutedQuantity) + "/" + trimTrailingZeroFloat(o.Quantity), + Value: o.ExecutedQuantity.String() + "/" + o.Quantity.String(), Short: true, }, } diff --git a/pkg/types/position.go b/pkg/types/position.go index 2313649aa5..d4db7d7ff2 100644 --- a/pkg/types/position.go +++ b/pkg/types/position.go @@ -53,11 +53,10 @@ type Position struct { sync.Mutex } -func (p *Position) NewClosePositionOrder(percentage float64) *SubmitOrder { +func (p *Position) NewClosePositionOrder(percentage fixedpoint.Value) *SubmitOrder { base := p.GetBase() - quantity := base.Float64() - quantity = quantity * percentage - if quantity < p.Market.MinQuantity { + quantity := base.Mul(percentage) + if quantity.Compare(p.Market.MinQuantity) < 0 { return nil } @@ -67,8 +66,6 @@ func (p *Position) NewClosePositionOrder(percentage float64) *SubmitOrder { return nil } else if sign < 0 { side = SideTypeBuy - } else if sign > 0 { - side = SideTypeSell } return &SubmitOrder{ @@ -189,9 +186,9 @@ func (p *Position) SlackAttachment() slack.Attachment { title := util.Render(string(posType)+` Position {{ .Symbol }} `, p) fields := []slack.AttachmentField{ - {Title: "Average Cost", Value: trimTrailingZeroFloat(averageCost.Float64()) + " " + p.QuoteCurrency, Short: true}, - {Title: p.BaseCurrency, Value: trimTrailingZeroFloat(base.Float64()), Short: true}, - {Title: p.QuoteCurrency, Value: trimTrailingZeroFloat(quote.Float64())}, + {Title: "Average Cost", Value: averageCost.String() + " " + p.QuoteCurrency, Short: true}, + {Title: p.BaseCurrency, Value: base.String(), Short: true}, + {Title: p.QuoteCurrency, Value: quote.String()}, } if p.TotalFee != nil { @@ -199,7 +196,7 @@ func (p *Position) SlackAttachment() slack.Attachment { if fee.Sign() > 0 { fields = append(fields, slack.AttachmentField{ Title: fmt.Sprintf("Fee (%s)", feeCurrency), - Value: trimTrailingZeroFloat(fee.Float64()), + Value: fee.String(), Short: true, }) } @@ -222,14 +219,14 @@ func (p *Position) PlainText() (msg string) { msg = fmt.Sprintf("%s Position %s: average cost = %s, base = %s, quote = %s", posType, p.Symbol, - trimTrailingZeroFloat(p.AverageCost.Float64()), - trimTrailingZeroFloat(p.Base.Float64()), - trimTrailingZeroFloat(p.Quote.Float64()), + p.AverageCost.String(), + p.Base.String(), + p.Quote.String(), ) if p.TotalFee != nil { for feeCurrency, fee := range p.TotalFee { - msg += fmt.Sprintf("\nfee (%s) = %s", feeCurrency, trimTrailingZeroFloat(fee.Float64())) + msg += fmt.Sprintf("\nfee (%s) = %s", feeCurrency, fee.String()) } } diff --git a/pkg/types/ticker.go b/pkg/types/ticker.go index ad3610a55b..fba23b3a30 100644 --- a/pkg/types/ticker.go +++ b/pkg/types/ticker.go @@ -2,15 +2,16 @@ package types import ( "time" + "github.com/c9s/bbgo/pkg/fixedpoint" ) type Ticker struct { Time time.Time - Volume float64 // `volume` from Max & binance - Last float64 // `last` from Max, `lastPrice` from binance - Open float64 // `open` from Max, `openPrice` from binance - High float64 // `high` from Max, `highPrice` from binance - Low float64 // `low` from Max, `lowPrice` from binance - Buy float64 // `buy` from Max, `bidPrice` from binance - Sell float64 // `sell` from Max, `askPrice` from binance + Volume fixedpoint.Value // `volume` from Max & binance + Last fixedpoint.Value // `last` from Max, `lastPrice` from binance + Open fixedpoint.Value // `open` from Max, `openPrice` from binance + High fixedpoint.Value // `high` from Max, `highPrice` from binance + Low fixedpoint.Value // `low` from Max, `lowPrice` from binance + Buy fixedpoint.Value // `buy` from Max, `bidPrice` from binance + Sell fixedpoint.Value // `sell` from Max, `askPrice` from binance } diff --git a/pkg/types/trade.go b/pkg/types/trade.go index 4fb910fdd5..bbe35baf5a 100644 --- a/pkg/types/trade.go +++ b/pkg/types/trade.go @@ -90,7 +90,7 @@ func (trade Trade) PositionChange() fixedpoint.Value { return fixedpoint.Zero } -func trimTrailingZero(a string) string { +/*func trimTrailingZero(a string) string { index := strings.Index(a, ".") if index == -1 { return a @@ -111,9 +111,9 @@ func trimTrailingZero(a string) string { return a } -func trimTrailingZeroFloat(a float64) string { +func trimTrailingZero(a float64) string { return trimTrailingZero(fmt.Sprintf("%f", a)) -} +}*/ // String is for console output func (trade Trade) String() string { @@ -121,10 +121,10 @@ func (trade Trade) String() string { trade.Exchange.String(), trade.Symbol, trade.Side, - trimTrailingZeroFloat(trade.Quantity.Float64()), - trimTrailingZeroFloat(trade.Price.Float64()), - trimTrailingZeroFloat(trade.QuoteQuantity.Float64()), - trimTrailingZeroFloat(trade.Fee.Float64()), + trade.Quantity.String(), + trade.Price.String(), + trade.QuoteQuantity.String(), + trade.Fee.String(), 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.Float64()), - trimTrailingZeroFloat(trade.Price.Float64()), - trimTrailingZeroFloat(trade.QuoteQuantity.Float64()), - trimTrailingZeroFloat(trade.Fee.Float64()), + trade.Quantity.String(), + trade.Price.String(), + trade.QuoteQuantity.String(), + trade.Fee.String(), 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.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: "Price", Value: trade.Price.String(), Short: true}, + {Title: "Quantity", Value: trade.Quantity.String(), Short: true}, + {Title: "QuoteQuantity", Value: trade.QuoteQuantity.String(), Short: true}, + {Title: "Fee", Value: trade.Fee.String(), 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/types/withdraw.go b/pkg/types/withdraw.go index 4e53392bb4..25d96ab60b 100644 --- a/pkg/types/withdraw.go +++ b/pkg/types/withdraw.go @@ -3,19 +3,20 @@ package types import ( "fmt" "time" + "github.com/c9s/bbgo/pkg/fixedpoint" ) type Withdraw struct { GID int64 `json:"gid" db:"gid"` Exchange ExchangeName `json:"exchange" db:"exchange"` Asset string `json:"asset" db:"asset"` - Amount float64 `json:"amount" db:"amount"` + Amount fixedpoint.Value `json:"amount" db:"amount"` Address string `json:"address" db:"address"` AddressTag string `json:"addressTag"` Status string `json:"status"` TransactionID string `json:"transactionID" db:"txn_id"` - TransactionFee float64 `json:"transactionFee" db:"txn_fee"` + TransactionFee fixedpoint.Value `json:"transactionFee" db:"txn_fee"` TransactionFeeCurrency string `json:"transactionFeeCurrency" db:"txn_fee_currency"` WithdrawOrderID string `json:"withdrawOrderId"` ApplyTime Time `json:"applyTime" db:"time"` diff --git a/pkg/util/math.go b/pkg/util/math.go index 04c457d46a..2de0f5f1c8 100644 --- a/pkg/util/math.go +++ b/pkg/util/math.go @@ -20,7 +20,7 @@ func Pow10(n int64) int64 { } func FormatValue(val fixedpoint.Value, prec int) string { - return strconv.FormatFloat(val.Float64(), 'f', prec, 64) + return val.FormatString(prec) } func FormatFloat(val float64, prec int) string {