mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-10 09:11:55 +00:00
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:
parent
336d86811f
commit
e221f54397
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)))
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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)),
|
||||
})
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
|
|
|
@ -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
985
pkg/fixedpoint/dec.go
Normal 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()
|
||||
}
|
|
@ -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
132
pkg/fixedpoint/div128.go
Normal 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)
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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},
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user