add dnum as the fixedpoint implementation. change types float64 to fixedpoint.Value

change pnl report to use fixedpoint

fix: migrate kline to use fixedpoint
This commit is contained in:
zenix 2022-02-02 20:37:18 +09:00
parent 336d86811f
commit e221f54397
26 changed files with 1623 additions and 816 deletions

View File

@ -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)
}

View File

@ -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,
}
}

View File

@ -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),

View File

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

View File

@ -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)))
})
}

View File

@ -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),

View File

@ -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)),
})

View File

@ -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),
}

View File

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

985
pkg/fixedpoint/dec.go Normal file
View File

@ -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()
}

View File

@ -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",

132
pkg/fixedpoint/div128.go Normal file
View File

@ -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)
}

View File

@ -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) {

View File

@ -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 {

View File

@ -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()

View File

@ -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) GetUpperShadowRatio() float64 {
return k.GetUpperShadowHeight() / math.Abs(k.GetMaxChange())
}
func (k KLine) GetUpperShadowHeight() float64 {
high := k.GetHigh()
if k.GetOpen() > k.GetClose() {
return high - k.GetOpen()
func (k KLine) GetThickness() fixedpoint.Value {
out := k.GetChange().Div(k.GetMaxChange())
if out.Sign() < 0 {
return out.Neg()
}
return high - k.GetClose()
return out
}
func (k KLine) GetLowerShadowRatio() float64 {
return k.GetLowerShadowHeight() / 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) GetLowerShadowHeight() float64 {
func (k KLine) GetUpperShadowHeight() fixedpoint.Value {
high := k.GetHigh()
open := k.GetOpen()
clos := k.GetClose()
if open.Compare(clos) > 0 {
return high.Sub(open)
}
return high.Sub(clos)
}
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() 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,
},
},

View File

@ -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())
}

View File

@ -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
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 0, 0, false
return fixedpoint.Zero, fixedpoint.Zero, false
}
return 0, 0, false
return fixedpoint.Zero, fixedpoint.Zero, false
}

View File

@ -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")
}
})
}

View File

@ -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

View File

@ -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)
}

View File

@ -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
})
}

View File

@ -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

View File

@ -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)

View File

@ -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},

View File

@ -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)
}