Files
gocryptotrader/common/math/math.go
Adrian Gallagher 9a4eb9de84 CI: Fix golangci-lint linter issues, add prealloc linter and bump version depends for Go 1.18 (#915)
* Bump CI versions

* Specifically set go version as 1.17.x bumps it to 1.18

* Another

* Adjust AppVeyor

* Part 1 of linter issues

* Part 2

* Fix various linters and improvements

* Part 3

* Finishing touches

* Tests and EqualFold

* Fix nitterinos plus bonus requester jobs bump for exchanges with large number of tests

* Fix nitterinos and bump golangci-lint timeout for AppVeyor

* Address nits, ensure all books are returned on err due to syncer regression

* Fix the wiggins

* Fix duplication

* Fix nitterinos
2022-04-20 13:45:15 +10:00

476 lines
17 KiB
Go

package math
import (
"errors"
"fmt"
"math"
"github.com/shopspring/decimal"
)
var (
// ErrNoNegativeResults is returned when no negative results are allowed
ErrNoNegativeResults = errors.New("cannot calculate with no negative values")
// ErrInexactConversion is returned when a decimal does not convert to float exactly
ErrInexactConversion = errors.New("inexact conversion from decimal to float detected")
errZeroValue = errors.New("cannot calculate average of no values")
errNegativeValueOutOfRange = errors.New("received negative number less than -1")
errGeometricNegative = errors.New("cannot calculate a geometric mean with negative values")
errCalmarHighest = errors.New("cannot calculate calmar ratio with highest price of 0")
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")
)
// CalculateAmountWithFee returns a calculated fee included amount on fee
func CalculateAmountWithFee(amount, fee float64) float64 {
return amount + CalculateFee(amount, fee)
}
// CalculateFee returns a simple fee on amount
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
}
// CalculatePercentageDifference returns the percentage of difference between
// multiple time periods
func CalculatePercentageDifference(amount, secondAmount float64) float64 {
return (amount - secondAmount) / ((amount + secondAmount) / 2) * 100
}
// CalculateNetProfit returns net profit
func CalculateNetProfit(amount, priceThen, priceNow, costs float64) float64 {
return (priceNow * amount) - (priceThen * amount) - costs
}
// RoundFloat rounds your floating point number to the desired decimal place
func RoundFloat(x float64, prec int) float64 {
pow := math.Pow(10, float64(prec))
return math.Round(x*pow) / pow
}
// CompoundAnnualGrowthRate Calculates CAGR.
// Using years, intervals per year would be 1 and number of intervals would be the number of years
// Using days, intervals per year would be 365 and number of intervals would be the number of days
func CompoundAnnualGrowthRate(openValue, closeValue, intervalsPerYear, numberOfIntervals float64) (float64, error) {
if numberOfIntervals == 0 {
return 0, errCAGRNoIntervals
}
if openValue == 0 {
return 0, errCAGRZeroOpenValue
}
k := math.Pow(closeValue/openValue, intervalsPerYear/numberOfIntervals) - 1
return k * 100, nil
}
// CalmarRatio is a function of the average compounded annual rate of return versus its maximum drawdown.
// The higher the Calmar ratio, the better it performed on a risk-adjusted basis during the given time frame, which is mostly commonly set at 36 months
func CalmarRatio(highestPrice, lowestPrice, average, riskFreeRateForPeriod float64) (float64, error) {
if highestPrice == 0 {
return 0, errCalmarHighest
}
drawdownDiff := (highestPrice - lowestPrice) / highestPrice
if drawdownDiff == 0 {
return 0, nil
}
return (average - riskFreeRateForPeriod) / drawdownDiff, nil
}
// InformationRatio The information ratio (IR) is a measurement of portfolio returns beyond the returns of a benchmark,
// usually an index, compared to the volatility of those returns.
// The benchmark used is typically an index that represents the market or a particular sector or industry.
func InformationRatio(returnsRates, benchmarkRates []float64, averageValues, averageComparison float64) (float64, error) {
if len(benchmarkRates) != len(returnsRates) {
return 0, errInformationBadLength
}
diffs := make([]float64, len(returnsRates))
for i := range returnsRates {
diffs[i] = returnsRates[i] - benchmarkRates[i]
}
stdDev, err := PopulationStandardDeviation(diffs)
if err != nil {
return 0, err
}
if stdDev == 0 {
return 0, nil
}
return (averageValues - averageComparison) / stdDev, nil
}
// PopulationStandardDeviation calculates standard deviation using population based calculation
func PopulationStandardDeviation(values []float64) (float64, error) {
if len(values) < 2 {
return 0, nil
}
valAvg, err := ArithmeticMean(values)
if err != nil {
return 0, err
}
diffs := make([]float64, len(values))
for x := range values {
diffs[x] = math.Pow(values[x]-valAvg, 2)
}
var diffAvg float64
diffAvg, err = ArithmeticMean(diffs)
if err != nil {
return 0, err
}
return math.Sqrt(diffAvg), nil
}
// SampleStandardDeviation standard deviation is a statistic that
// measures the dispersion of a dataset relative to its mean and
// is calculated as the square root of the variance
func SampleStandardDeviation(values []float64) (float64, error) {
if len(values) < 2 {
return 0, nil
}
mean, err := ArithmeticMean(values)
if err != nil {
return 0, err
}
superMean := make([]float64, len(values))
var combined float64
for i := range values {
result := math.Pow(values[i]-mean, 2)
superMean[i] = result
combined += result
}
avg := combined / (float64(len(superMean)) - 1)
return math.Sqrt(avg), nil
}
// GeometricMean is an average which indicates the central tendency or
// typical value of a set of numbers by using the product of their values
// The geometric average can only process positive numbers
func GeometricMean(values []float64) (float64, error) {
if len(values) == 0 {
return 0, errZeroValue
}
product := 1.0
for i := range values {
if values[i] <= 0 {
// cannot use negative or zero values in geometric calculation
return 0, errGeometricNegative
}
product *= values[i]
}
geometricPower := math.Pow(product, 1/float64(len(values)))
return geometricPower, nil
}
// FinancialGeometricMean is a modified geometric average to assess
// the negative returns of investments. It accepts It adds +1 to each
// This does impact the final figures as it is modifying values
// It is still ultimately calculating a geometric average
// which should only be compared to other financial geometric averages
func FinancialGeometricMean(values []float64) (float64, error) {
if len(values) == 0 {
return 0, errZeroValue
}
product := 1.0
for i := range values {
if values[i] < -1 {
// cannot lose more than 100%, figures are incorrect
// losing exactly 100% will return a 0 value, but is not an error
return 0, errNegativeValueOutOfRange
}
// 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] + 1
product *= modVal
}
prod := 1 / float64(len(values))
geometricPower := math.Pow(product, prod)
if geometricPower > 0 {
// we minus 1 because we manipulated the values to be non-zero/negative
geometricPower--
}
return geometricPower, nil
}
// ArithmeticMean is the basic form of calculating an average.
// Divide the sum of all values by the length of values
func ArithmeticMean(values []float64) (float64, error) {
if len(values) == 0 {
return 0, errZeroValue
}
var sumOfValues float64
for x := range values {
sumOfValues += values[x]
}
return sumOfValues / float64(len(values)), nil
}
// SortinoRatio returns sortino ratio of backtest compared to risk-free
func SortinoRatio(movementPerCandle []float64, riskFreeRatePerInterval, average float64) (float64, error) {
totalIntervals := float64(len(movementPerCandle))
if totalIntervals == 0 {
return 0, errZeroValue
}
totalNegativeResultsSquared := 0.0
for x := range movementPerCandle {
if movementPerCandle[x]-riskFreeRatePerInterval < 0 {
totalNegativeResultsSquared += math.Pow(movementPerCandle[x]-riskFreeRatePerInterval, 2)
}
}
averageDownsideDeviation := math.Sqrt(totalNegativeResultsSquared / float64(len(movementPerCandle)))
return (average - riskFreeRatePerInterval) / averageDownsideDeviation, nil
}
// SharpeRatio returns sharpe ratio of backtest compared to risk-free
func SharpeRatio(movementPerCandle []float64, riskFreeRatePerInterval, average float64) (float64, error) {
totalIntervals := float64(len(movementPerCandle))
if totalIntervals == 0 {
return 0, errZeroValue
}
excessReturns := make([]float64, len(movementPerCandle))
for i := range movementPerCandle {
excessReturns[i] = movementPerCandle[i] - riskFreeRatePerInterval
}
standardDeviation, err := PopulationStandardDeviation(excessReturns)
if err != nil {
return 0, err
}
if standardDeviation == 0 {
return 0, nil
}
return (average - riskFreeRatePerInterval) / standardDeviation, nil
}
// DecimalCompoundAnnualGrowthRate Calculates CAGR.
// Using years, intervals per year would be 1 and number of intervals would be the number of years
// Using days, intervals per year would be 365 and number of intervals would be the number of days
func DecimalCompoundAnnualGrowthRate(openValue, closeValue, intervalsPerYear, numberOfIntervals decimal.Decimal) (decimal.Decimal, error) {
if numberOfIntervals.IsZero() {
return decimal.Zero, errCAGRNoIntervals
}
if openValue.IsZero() {
return decimal.Zero, errCAGRZeroOpenValue
}
closeOverOpen := closeValue.Div(openValue)
exp := intervalsPerYear.Div(numberOfIntervals)
pow := DecimalPow(closeOverOpen, exp)
k := pow.Sub(decimal.NewFromInt(1)).Mul(decimal.NewFromInt(100))
return k, nil
}
// DecimalCalmarRatio is a function of the average compounded annual rate of return versus its maximum drawdown.
// The higher the Calmar ratio, the better it performed on a risk-adjusted basis during the given time frame, which is mostly commonly set at 36 months
func DecimalCalmarRatio(highestPrice, lowestPrice, average, riskFreeRateForPeriod decimal.Decimal) (decimal.Decimal, error) {
if highestPrice.IsZero() {
return decimal.Zero, errCalmarHighest
}
drawdownDiff := highestPrice.Sub(lowestPrice).Div(highestPrice)
if drawdownDiff.IsZero() {
return decimal.Zero, nil
}
return average.Sub(riskFreeRateForPeriod).Div(drawdownDiff), nil
}
// DecimalInformationRatio The information ratio (IR) is a measurement of portfolio returns beyond the returns of a benchmark,
// usually an index, compared to the volatility of those returns.
// The benchmark used is typically an index that represents the market or a particular sector or industry.
func DecimalInformationRatio(returnsRates, benchmarkRates []decimal.Decimal, averageValues, averageComparison decimal.Decimal) (decimal.Decimal, error) {
if len(benchmarkRates) != len(returnsRates) {
return decimal.Zero, errInformationBadLength
}
diffs := make([]decimal.Decimal, len(returnsRates))
for i := range returnsRates {
diffs[i] = returnsRates[i].Sub(benchmarkRates[i])
}
stdDev, err := DecimalPopulationStandardDeviation(diffs)
if err != nil && !errors.Is(err, ErrInexactConversion) {
return decimal.Zero, err
}
if stdDev.IsZero() {
return decimal.Zero, nil
}
return averageValues.Sub(averageComparison).Div(stdDev), nil
}
// DecimalPopulationStandardDeviation calculates standard deviation using population based calculation
func DecimalPopulationStandardDeviation(values []decimal.Decimal) (decimal.Decimal, error) {
if len(values) < 2 {
return decimal.Zero, nil
}
valAvg, err := DecimalArithmeticMean(values)
if err != nil {
return decimal.Zero, err
}
diffs := make([]decimal.Decimal, len(values))
for x := range values {
val := values[x].Sub(valAvg)
exp := decimal.NewFromInt(2)
pow := DecimalPow(val, exp)
diffs[x] = pow
}
var diffAvg decimal.Decimal
diffAvg, err = DecimalArithmeticMean(diffs)
if err != nil {
return decimal.Zero, err
}
f, exact := diffAvg.Float64()
err = nil
if !exact {
err = fmt.Errorf("%w from %v to %v", ErrInexactConversion, diffAvg, f)
}
resp := decimal.NewFromFloat(math.Sqrt(f))
return resp, err
}
// DecimalSampleStandardDeviation standard deviation is a statistic that
// measures the dispersion of a dataset relative to its mean and
// is calculated as the square root of the variance
func DecimalSampleStandardDeviation(values []decimal.Decimal) (decimal.Decimal, error) {
if len(values) < 2 {
return decimal.Zero, nil
}
mean, err := DecimalArithmeticMean(values)
if err != nil {
return decimal.Zero, err
}
superMean := make([]decimal.Decimal, len(values))
var combined decimal.Decimal
for i := range values {
pow := values[i].Sub(mean).Pow(decimal.NewFromInt(2))
superMean[i] = pow
combined.Add(pow)
}
avg := combined.Div(decimal.NewFromInt(int64(len(superMean))).Sub(decimal.NewFromInt(1)))
f, exact := avg.Float64()
err = nil
if !exact {
err = fmt.Errorf("%w from %v to %v", ErrInexactConversion, avg, f)
}
sqrt := math.Sqrt(f)
return decimal.NewFromFloat(sqrt), err
}
// DecimalGeometricMean is an average which indicates the central tendency or
// typical value of a set of numbers by using the product of their values
// The geometric average can only process positive numbers
func DecimalGeometricMean(values []decimal.Decimal) (decimal.Decimal, error) {
if len(values) == 0 {
return decimal.Zero, errZeroValue
}
product := decimal.NewFromInt(1)
for i := range values {
if values[i].LessThanOrEqual(decimal.Zero) {
// cannot use negative or zero values in geometric calculation
return decimal.Zero, errGeometricNegative
}
product = product.Mul(values[i])
}
exp := decimal.NewFromInt(1).Div(decimal.NewFromInt(int64(len(values))))
pow := DecimalPow(product, exp)
geometricPower := pow
return geometricPower, nil
}
// DecimalPow is lovely because shopspring decimal cannot
// handle ^0.x and instead returns 1
func DecimalPow(x, y decimal.Decimal) decimal.Decimal {
pow := math.Pow(x.InexactFloat64(), y.InexactFloat64())
return decimal.NewFromFloat(pow)
}
// DecimalFinancialGeometricMean is a modified geometric average to assess
// the negative returns of investments. It accepts It adds +1 to each
// This does impact the final figures as it is modifying values
// It is still ultimately calculating a geometric average
// which should only be compared to other financial geometric averages
func DecimalFinancialGeometricMean(values []decimal.Decimal) (decimal.Decimal, error) {
if len(values) == 0 {
return decimal.Zero, errZeroValue
}
product := 1.0
for i := range values {
if values[i].LessThan(decimal.NewFromInt(-1)) {
// cannot lose more than 100%, figures are incorrect
// losing exactly 100% will return a 0 value, but is not an error
return decimal.Zero, errNegativeValueOutOfRange
}
// 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()
product *= modVal
}
prod := 1 / float64(len(values))
geometricPower := math.Pow(product, prod)
if geometricPower > 0 {
// we minus 1 because we manipulated the values to be non-zero/negative
geometricPower--
}
return decimal.NewFromFloat(geometricPower), nil
}
// DecimalArithmeticMean is the basic form of calculating an average.
// Divide the sum of all values by the length of values
func DecimalArithmeticMean(values []decimal.Decimal) (decimal.Decimal, error) {
if len(values) == 0 {
return decimal.Zero, errZeroValue
}
var sumOfValues decimal.Decimal
for x := range values {
sumOfValues = sumOfValues.Add(values[x])
}
return sumOfValues.Div(decimal.NewFromInt(int64(len(values)))), nil
}
// DecimalSortinoRatio returns sortino ratio of backtest compared to risk-free
func DecimalSortinoRatio(movementPerCandle []decimal.Decimal, riskFreeRatePerInterval, average decimal.Decimal) (decimal.Decimal, error) {
if len(movementPerCandle) == 0 {
return decimal.Zero, errZeroValue
}
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)))
}
}
if totalNegativeResultsSquared.IsZero() {
return decimal.Zero, ErrNoNegativeResults
}
f, exact := totalNegativeResultsSquared.Float64()
var err error
if !exact {
err = fmt.Errorf("%w from %v to %v", ErrInexactConversion, totalNegativeResultsSquared, f)
}
fAverageDownsideDeviation := math.Sqrt(f / float64(len(movementPerCandle)))
averageDownsideDeviation := decimal.NewFromFloat(fAverageDownsideDeviation)
return average.Sub(riskFreeRatePerInterval).Div(averageDownsideDeviation), err
}
// DecimalSharpeRatio returns sharpe ratio of backtest compared to risk-free
func DecimalSharpeRatio(movementPerCandle []decimal.Decimal, riskFreeRatePerInterval, average decimal.Decimal) (decimal.Decimal, error) {
totalIntervals := decimal.NewFromInt(int64(len(movementPerCandle)))
if totalIntervals.IsZero() {
return decimal.Zero, errZeroValue
}
excessReturns := make([]decimal.Decimal, len(movementPerCandle))
for i := range movementPerCandle {
excessReturns[i] = movementPerCandle[i].Sub(riskFreeRatePerInterval)
}
standardDeviation, err := DecimalPopulationStandardDeviation(excessReturns)
if err != nil && !errors.Is(err, ErrInexactConversion) {
return decimal.Zero, err
}
if standardDeviation.IsZero() {
return decimal.Zero, nil
}
return average.Sub(riskFreeRatePerInterval).Div(standardDeviation), nil
}