From a0d82f2a7dc754e462656b14f73f68198e4cf676 Mon Sep 17 00:00:00 2001 From: Ryan O'Hara-Reid Date: Wed, 27 Nov 2024 10:27:15 +1100 Subject: [PATCH] common/math: Add math.Abs to PercentageDifference calculation (#1617) * fix bug and add decimal calc * pew pew * Update common/math/math.go Co-authored-by: Scott * glorious: nits * nits: plus change name convention * gk: nits and splits --------- Co-authored-by: Ryan O'Hara-Reid Co-authored-by: Scott --- common/math/math.go | 42 ++++++++++++++--------- common/math/math_test.go | 56 ++++++++++++++++++++++--------- engine/datahistory_manager.go | 6 ++-- exchanges/orderbook/calculator.go | 6 ++-- exchanges/orderbook/tranches.go | 12 +++---- 5 files changed, 78 insertions(+), 44 deletions(-) diff --git a/common/math/math.go b/common/math/math.go index 376f35ad..5fdb47f4 100644 --- a/common/math/math.go +++ b/common/math/math.go @@ -24,6 +24,10 @@ var ( errCAGRNoIntervals = errors.New("cannot calculate CAGR with no intervals") errCAGRZeroOpenValue = errors.New("cannot calculate CAGR with an open value of 0") errInformationBadLength = errors.New("benchmark rates length does not match returns rates") + + one = decimal.NewFromInt(1) + two = decimal.NewFromInt(2) + oneHundred = decimal.NewFromInt(100) ) // CalculateAmountWithFee returns a calculated fee included amount on fee @@ -36,16 +40,22 @@ func CalculateFee(amount, fee float64) float64 { return amount * (fee / 100) } -// CalculatePercentageGainOrLoss returns the percentage rise over a certain -// period -func CalculatePercentageGainOrLoss(priceNow, priceThen float64) float64 { - return (priceNow - priceThen) / priceThen * 100 +// PercentageChange returns the percentage change between two numbers, x is reference value. +func PercentageChange(x, y float64) float64 { + return (y - x) / x * 100 } -// CalculatePercentageDifference returns the percentage of difference between -// multiple time periods -func CalculatePercentageDifference(amount, secondAmount float64) float64 { - return (amount - secondAmount) / ((amount + secondAmount) / 2) * 100 +// PercentageDifference returns difference between two numbers as a percentage of their average +func PercentageDifference(x, y float64) float64 { + return math.Abs(x-y) / ((x + y) / 2) * 100 +} + +// PercentageDifferenceDecimal returns the difference between two decimal values as a percentage of their average +func PercentageDifferenceDecimal(x, y decimal.Decimal) decimal.Decimal { + if x.IsZero() && y.IsZero() { + return decimal.Zero + } + return x.Sub(y).Abs().Div(x.Add(y).Div(two)).Mul(oneHundred) } // CalculateNetProfit returns net profit @@ -267,7 +277,7 @@ func DecimalCompoundAnnualGrowthRate(openValue, closeValue, intervalsPerYear, nu if pow.IsZero() { return decimal.Zero, ErrPowerDifferenceTooSmall } - k := pow.Sub(decimal.NewFromInt(1)).Mul(decimal.NewFromInt(100)) + k := pow.Sub(one).Mul(oneHundred) return k, nil } @@ -317,7 +327,7 @@ func DecimalPopulationStandardDeviation(values []decimal.Decimal) (decimal.Decim diffs := make([]decimal.Decimal, len(values)) for x := range values { val := values[x].Sub(valAvg) - exp := decimal.NewFromInt(2) + exp := two pow := DecimalPow(val, exp) diffs[x] = pow } @@ -349,11 +359,11 @@ func DecimalSampleStandardDeviation(values []decimal.Decimal) (decimal.Decimal, superMean := make([]decimal.Decimal, len(values)) var combined decimal.Decimal for i := range values { - pow := values[i].Sub(mean).Pow(decimal.NewFromInt(2)) + pow := values[i].Sub(mean).Pow(two) superMean[i] = pow combined.Add(pow) } - avg := combined.Div(decimal.NewFromInt(int64(len(superMean))).Sub(decimal.NewFromInt(1))) + avg := combined.Div(decimal.NewFromInt(int64(len(superMean))).Sub(one)) f, exact := avg.Float64() err = nil if !exact { @@ -370,7 +380,7 @@ func DecimalGeometricMean(values []decimal.Decimal) (decimal.Decimal, error) { if len(values) == 0 { return decimal.Zero, errZeroValue } - product := decimal.NewFromInt(1) + product := one for i := range values { if values[i].LessThanOrEqual(decimal.Zero) { // cannot use negative or zero values in geometric calculation @@ -378,7 +388,7 @@ func DecimalGeometricMean(values []decimal.Decimal) (decimal.Decimal, error) { } product = product.Mul(values[i]) } - exp := decimal.NewFromInt(1).Div(decimal.NewFromInt(int64(len(values)))) + exp := one.Div(decimal.NewFromInt(int64(len(values)))) pow := DecimalPow(product, exp) geometricPower := pow return geometricPower, nil @@ -413,7 +423,7 @@ func DecimalFinancialGeometricMean(values []decimal.Decimal) (decimal.Decimal, e // as we cannot have negative or zero value geometric numbers // adding a 1 to the percentage movements allows for differentiation between // negative numbers (eg -0.1 translates to 0.9) and positive numbers (eg 0.1 becomes 1.1) - modVal := values[i].Add(decimal.NewFromInt(1)).InexactFloat64() + modVal := values[i].Add(one).InexactFloat64() product *= modVal } prod := 1 / float64(len(values)) @@ -446,7 +456,7 @@ func DecimalSortinoRatio(movementPerCandle []decimal.Decimal, riskFreeRatePerInt totalNegativeResultsSquared := decimal.Zero for x := range movementPerCandle { if movementPerCandle[x].Sub(riskFreeRatePerInterval).LessThan(decimal.Zero) { - totalNegativeResultsSquared = totalNegativeResultsSquared.Add(movementPerCandle[x].Sub(riskFreeRatePerInterval).Pow(decimal.NewFromInt(2))) + totalNegativeResultsSquared = totalNegativeResultsSquared.Add(movementPerCandle[x].Sub(riskFreeRatePerInterval).Pow(two)) } } if totalNegativeResultsSquared.IsZero() { diff --git a/common/math/math_test.go b/common/math/math_test.go index ed21cf3e..4888ebad 100644 --- a/common/math/math_test.go +++ b/common/math/math_test.go @@ -6,6 +6,8 @@ import ( "testing" "github.com/shopspring/decimal" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestCalculateFee(t *testing.T) { @@ -28,27 +30,49 @@ func TestCalculateAmountWithFee(t *testing.T) { } } -func TestCalculatePercentageGainOrLoss(t *testing.T) { +func TestPercentageChange(t *testing.T) { t.Parallel() - originalInput := float64(9300) - secondInput := float64(9000) - expectedOutput := 3.3333333333333335 - actualResult := CalculatePercentageGainOrLoss(originalInput, secondInput) - if expectedOutput != actualResult { - t.Errorf( - "Expected '%v'. Actual '%v'.", expectedOutput, actualResult) + assert.Equal(t, 3.3333333333333335, PercentageChange(9000, 9300)) + assert.Equal(t, -3.225806451612903, PercentageChange(9300, 9000)) + assert.True(t, math.IsNaN(PercentageChange(0, 0))) + assert.Equal(t, 0.0, PercentageChange(1, 1)) + assert.Equal(t, 0.0, PercentageChange(-1, -1)) + assert.True(t, math.IsInf(PercentageChange(0, 1), 1)) + assert.Equal(t, -100., PercentageChange(1, 0)) +} + +func TestPercentageDifference(t *testing.T) { + t.Parallel() + require.Equal(t, 196.03960396039605, PercentageDifference(1, 100)) + require.Equal(t, 196.03960396039605, PercentageDifference(100, 1)) + require.Equal(t, 0.13605442176870758, PercentageDifference(1.469, 1.471)) + require.Equal(t, 0.13605442176870758, PercentageDifference(1.471, 1.469)) + require.Equal(t, 0.0, PercentageDifference(1.0, 1.0)) + require.True(t, math.IsNaN(PercentageDifference(0.0, 0.0))) +} + +// 1000000000 0.2215 ns/op 0 B/op 0 allocs/op +func BenchmarkPercentageDifference(b *testing.B) { + for i := 0; i < b.N; i++ { + PercentageDifference(1.469, 1.471) } } -func TestCalculatePercentageDifference(t *testing.T) { +func TestPercentageDifferenceDecimal(t *testing.T) { t.Parallel() - originalInput := float64(10) - secondAmount := float64(5) - expectedOutput := 66.66666666666666 - actualResult := CalculatePercentageDifference(originalInput, secondAmount) - if expectedOutput != actualResult { - t.Errorf( - "Expected '%f'. Actual '%f'.", expectedOutput, actualResult) + require.Equal(t, "196.03960396039604", PercentageDifferenceDecimal(decimal.NewFromFloat(1), decimal.NewFromFloat(100)).String()) + require.Equal(t, "196.03960396039604", PercentageDifferenceDecimal(decimal.NewFromFloat(100), decimal.NewFromFloat(1)).String()) + require.Equal(t, "0.13605442176871", PercentageDifferenceDecimal(decimal.NewFromFloat(1.469), decimal.NewFromFloat(1.471)).String()) + require.Equal(t, "0.13605442176871", PercentageDifferenceDecimal(decimal.NewFromFloat(1.471), decimal.NewFromFloat(1.469)).String()) + require.Equal(t, "0", PercentageDifferenceDecimal(decimal.NewFromFloat(1.0), decimal.NewFromFloat(1.0)).String()) + require.Equal(t, "0", PercentageDifferenceDecimal(decimal.Zero, decimal.Zero).String()) +} + +// 1585596 751.8 ns/op 792 B/op 27 allocs/op +func BenchmarkDecimalPercentageDifference(b *testing.B) { + d1, d2 := decimal.NewFromFloat(1.469), decimal.NewFromFloat(1.471) + for i := 0; i < b.N; i++ { + PercentageDifferenceDecimal(d1, d2) } } diff --git a/engine/datahistory_manager.go b/engine/datahistory_manager.go index c2810874..61a37b2a 100644 --- a/engine/datahistory_manager.go +++ b/engine/datahistory_manager.go @@ -1049,10 +1049,10 @@ func (m *DataHistoryManager) CheckCandleIssue(job *DataHistoryJob, multiplier in } if apiData != dbData { var diff float64 - if apiData > dbData { - diff = gctmath.CalculatePercentageGainOrLoss(apiData, dbData) + if apiData < dbData { + diff = gctmath.PercentageChange(apiData, dbData) } else { - diff = gctmath.CalculatePercentageGainOrLoss(dbData, apiData) + diff = gctmath.PercentageChange(dbData, apiData) } if diff > job.IssueTolerancePercentage { issue = fmt.Sprintf("%s api: %v db: %v diff: %v %%", candleField, apiData, dbData, diff) diff --git a/exchanges/orderbook/calculator.go b/exchanges/orderbook/calculator.go index 745dfdfd..8fa1123d 100644 --- a/exchanges/orderbook/calculator.go +++ b/exchanges/orderbook/calculator.go @@ -46,7 +46,7 @@ func (b *Base) WhaleBomb(priceTarget float64, buy bool) (*WhaleBombResult, error minPrice = action.ReferencePrice maxPrice = action.TranchePositionPrice amount = action.QuoteAmount - percent = math.CalculatePercentageGainOrLoss(action.TranchePositionPrice, action.ReferencePrice) + percent = math.PercentageChange(action.ReferencePrice, action.TranchePositionPrice) status = fmt.Sprintf("Buying using %.2f %s worth of %s will send the price from %v to %v [%.2f%%] and impact %d price tranche(s). %s", amount, b.Pair.Quote, b.Pair.Base, minPrice, maxPrice, percent, len(action.Tranches), warning) @@ -54,7 +54,7 @@ func (b *Base) WhaleBomb(priceTarget float64, buy bool) (*WhaleBombResult, error minPrice = action.TranchePositionPrice maxPrice = action.ReferencePrice amount = action.BaseAmount - percent = math.CalculatePercentageGainOrLoss(action.TranchePositionPrice, action.ReferencePrice) + percent = math.PercentageChange(action.ReferencePrice, action.TranchePositionPrice) status = fmt.Sprintf("Selling using %.2f %s worth of %s will send the price from %v to %v [%.2f%%] and impact %d price tranche(s). %s", amount, b.Pair.Base, b.Pair.Quote, maxPrice, minPrice, percent, len(action.Tranches), warning) @@ -108,7 +108,7 @@ func (b *Base) SimulateOrder(amount float64, buy bool) (*WhaleBombResult, error) warning = fullLiquidityUsageWarning } - pct := math.CalculatePercentageGainOrLoss(action.TranchePositionPrice, action.ReferencePrice) + pct := math.PercentageChange(action.ReferencePrice, action.TranchePositionPrice) status := fmt.Sprintf("%s using %f %v worth of %v will send the price from %v to %v [%.2f%%] and impact %v price tranche(s). %s", direction, soldAmount, sold, bought, action.ReferencePrice, action.TranchePositionPrice, pct, len(action.Tranches), warning) diff --git a/exchanges/orderbook/tranches.go b/exchanges/orderbook/tranches.go index b077440a..a3290e51 100644 --- a/exchanges/orderbook/tranches.go +++ b/exchanges/orderbook/tranches.go @@ -400,7 +400,7 @@ func (bids *bidTranches) hitBidsByNominalSlippage(slippage, refPrice float64) (* currentTotalAmounts := cumulativeAmounts + bids.Tranches[x].Amount nominal.AverageOrderCost = currentFullValue / currentTotalAmounts - percent := math.CalculatePercentageGainOrLoss(nominal.AverageOrderCost, refPrice) + percent := math.PercentageChange(refPrice, nominal.AverageOrderCost) if percent != 0 { percent *= -1 } @@ -461,7 +461,7 @@ func (bids *bidTranches) hitBidsByImpactSlippage(slippage, refPrice float64) (*M impact := &Movement{StartPrice: refPrice, EndPrice: refPrice} for x := range bids.Tranches { - percent := math.CalculatePercentageGainOrLoss(bids.Tranches[x].Price, refPrice) + percent := math.PercentageChange(refPrice, bids.Tranches[x].Price) if percent != 0 { percent *= -1 } @@ -529,7 +529,7 @@ func (ask *askTranches) liftAsksByNominalSlippage(slippage, refPrice float64) (* currentAmounts := cumulativeAmounts + ask.Tranches[x].Amount nominal.AverageOrderCost = currentValue / currentAmounts - percent := math.CalculatePercentageGainOrLoss(nominal.AverageOrderCost, refPrice) + percent := math.PercentageChange(refPrice, nominal.AverageOrderCost) if slippage < percent { targetCost := (1 + slippage/100) * refPrice @@ -582,7 +582,7 @@ func (ask *askTranches) liftAsksByImpactSlippage(slippage, refPrice float64) (*M impact := &Movement{StartPrice: refPrice, EndPrice: refPrice} for x := range ask.Tranches { - percent := math.CalculatePercentageGainOrLoss(ask.Tranches[x].Price, refPrice) + percent := math.PercentageChange(refPrice, ask.Tranches[x].Price) impact.ImpactPercentage = percent impact.EndPrice = ask.Tranches[x].Price if slippage <= percent { @@ -625,7 +625,7 @@ func (m *Movement) finalizeFields(cost, amount, headPrice, leftover float64, swa // Nominal percentage is the difference from the reference price to average // order cost. - m.NominalPercentage = math.CalculatePercentageGainOrLoss(m.AverageOrderCost, m.StartPrice) + m.NominalPercentage = math.PercentageChange(m.StartPrice, m.AverageOrderCost) if m.NominalPercentage < 0 { m.NominalPercentage *= -1 } @@ -634,7 +634,7 @@ func (m *Movement) finalizeFields(cost, amount, headPrice, leftover float64, swa // Impact percentage is how much the orderbook slips from the reference // price to the remaining tranche price. - m.ImpactPercentage = math.CalculatePercentageGainOrLoss(m.EndPrice, m.StartPrice) + m.ImpactPercentage = math.PercentageChange(m.StartPrice, m.EndPrice) if m.ImpactPercentage < 0 { m.ImpactPercentage *= -1 }