mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-05-14 07:26:47 +00:00
* 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
476 lines
17 KiB
Go
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
|
|
}
|