package math import ( "errors" "math" ) var ( 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 } var diffs []float64 for i := range returnsRates { diffs = append(diffs, 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 } var superMean []float64 var combined float64 for i := range values { result := math.Pow(values[i]-mean, 2) superMean = append(superMean, 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 } geometricPower := math.Pow(product, 1/float64(len(values))) 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 } var excessReturns []float64 for i := range movementPerCandle { excessReturns = append(excessReturns, movementPerCandle[i]-riskFreeRatePerInterval) } standardDeviation, err := PopulationStandardDeviation(excessReturns) if err != nil { return 0, err } if standardDeviation == 0 { return 0, nil } return (average - riskFreeRatePerInterval) / standardDeviation, nil }