/* * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Net; using MathNet.Numerics.Statistics; using QuantConnect.Logging; namespace QuantConnect.Statistics { /// /// Calculate all the statistics required from the backtest, based on the equity curve and the profit loss statement. /// /// This is a particularly ugly class and one of the first ones written. It should be thrown out and re-written. public class Statistics { /// /// Retrieve a static S-P500 Benchmark for the statistics calculations. Update the benchmark once per day. /// public static SortedDictionary YahooSPYBenchmark { get { var benchmark = new SortedDictionary(); var url = "http://real-chart.finance.yahoo.com/table.csv?s=SPY&a=11&b=31&c=1997&d=" + (DateTime.Now.Month - 1) + "&e=" + DateTime.Now.Day + "&f=" + DateTime.Now.Year + "&g=d&ignore=.csv"; using (var net = new WebClient()) { net.Proxy = WebRequest.GetSystemWebProxy(); var data = net.DownloadString(url); var first = true; using (var sr = new StreamReader(data.ToStream())) { while (sr.Peek() >= 0) { var line = sr.ReadLine(); if (first) { first = false; continue; } if (line == null) continue; var csv = line.Split(','); benchmark.Add(DateTime.Parse(csv[0]), Convert.ToDecimal(csv[6], CultureInfo.InvariantCulture)); } } } return benchmark; } } /// /// Convert the charting data into an equity array. /// /// This is required to convert the equity plot into a usable form for the statistics calculation /// ChartPoints Array /// SortedDictionary of the equity decimal values ordered in time private static SortedDictionary ChartPointToDictionary(IEnumerable points) { var dictionary = new SortedDictionary(); try { foreach (var point in points) { var x = Time.UnixTimeStampToDateTime(point.x); if (!dictionary.ContainsKey(x)) { dictionary.Add(x, point.y); } else { dictionary[x] = point.y; } } } catch (Exception err) { Log.Error(err); } return dictionary; } /// /// Run a full set of orders and return a Dictionary of statistics. /// /// Equity value over time. /// profit loss from trades /// Daily performance /// Benchmark data as dictionary. Data does not need to be ordered /// Amount of starting cash in USD /// The total fees incurred over the life time of the algorithm /// Total number of orders executed. /// Number of trading days per year /// Statistics Array, Broken into Annual Periods public static Dictionary Generate(IEnumerable pointsEquity, SortedDictionary profitLoss, IEnumerable pointsPerformance, Dictionary unsortedBenchmark, decimal startingCash, decimal totalFees, decimal totalTrades, double tradingDaysPerYear = 252 ) { //Initialise the response: double riskFreeRate = 0; decimal totalClosedTrades = 0; decimal totalWins = 0; decimal totalLosses = 0; decimal averageWin = 0; decimal averageLoss = 0; decimal averageWinRatio = 0; decimal winRate = 0; decimal lossRate = 0; decimal totalNetProfit = 0; double fractionOfYears = 1; decimal profitLossValue = 0, runningCash = startingCash; decimal algoCompoundingPerformance = 0; decimal finalBenchmarkCash = 0; decimal benchCompoundingPerformance = 0; var years = new List(); var annualTrades = new SortedDictionary(); var annualWins = new SortedDictionary(); var annualLosses = new SortedDictionary(); var annualLossTotal = new SortedDictionary(); var annualWinTotal = new SortedDictionary(); var annualNetProfit = new SortedDictionary(); var statistics = new Dictionary(); var dtPrevious = new DateTime(); var listPerformance = new List(); var listBenchmark = new List(); var equity = new SortedDictionary(); var performance = new SortedDictionary(); SortedDictionary benchmark = null; try { //Get array versions of the performance: performance = ChartPointToDictionary(pointsPerformance); equity = ChartPointToDictionary(pointsEquity); performance.Values.ToList().ForEach(i => listPerformance.Add((double)(i / 100))); benchmark = new SortedDictionary(unsortedBenchmark); // to find the delta in benchmark for first day, we need to know the price at the opening // moment of the day, but since we cannot find this, we cannot find the first benchmark's delta, // so we pad it with Zero. If running a short backtest this will skew results, longer backtests // will not be affected much listBenchmark.Add(0); //Get benchmark performance array for same period: benchmark.Keys.ToList().ForEach(dt => { if (dt >= equity.Keys.FirstOrDefault().AddDays(-1) && dt < equity.Keys.LastOrDefault()) { decimal previous; if (benchmark.TryGetValue(dtPrevious, out previous) && previous != 0) { var deltaBenchmark = (benchmark[dt] - previous)/previous; listBenchmark.Add((double)(deltaBenchmark)); } else { listBenchmark.Add(0); } dtPrevious = dt; } }); // TODO : if these lists are required to be the same length then we should create structure to pair the values, this way, by contract it will be enforced. //THIS SHOULD NEVER HAPPEN --> But if it does, log it and fail silently. while (listPerformance.Count < listBenchmark.Count) { listPerformance.Add(0); Log.Error("Statistics.Generate(): Padded Performance"); } while (listPerformance.Count > listBenchmark.Count) { listBenchmark.Add(0); Log.Error("Statistics.Generate(): Padded Benchmark"); } } catch (Exception err) { Log.Error(err, "Dic-Array Convert:"); } try { //Number of years in this dataset: fractionOfYears = (equity.Keys.LastOrDefault() - equity.Keys.FirstOrDefault()).TotalDays / 365; } catch (Exception err) { Log.Error(err, "Fraction of Years:"); } try { if (benchmark != null) { algoCompoundingPerformance = CompoundingAnnualPerformance(startingCash, equity.Values.LastOrDefault(), (decimal) fractionOfYears); finalBenchmarkCash = ((benchmark.Values.Last() - benchmark.Values.First())/benchmark.Values.First())*startingCash; benchCompoundingPerformance = CompoundingAnnualPerformance(startingCash, finalBenchmarkCash, (decimal) fractionOfYears); } } catch (Exception err) { Log.Error(err, "Compounding:"); } try { //Run over each equity day: foreach (var closedTrade in profitLoss.Keys) { profitLossValue = profitLoss[closedTrade]; //Check if this date is in the "years" array: var year = closedTrade.Year; if (!years.Contains(year)) { //Initialise a new year holder: years.Add(year); annualTrades.Add(year, 0); annualWins.Add(year, 0); annualWinTotal.Add(year, 0); annualLosses.Add(year, 0); annualLossTotal.Add(year, 0); } //Add another trade: annualTrades[year]++; //Profit loss tracking: if (profitLossValue > 0) { annualWins[year]++; annualWinTotal[year] += profitLossValue / runningCash; } else { annualLosses[year]++; annualLossTotal[year] += profitLossValue / runningCash; } //Increment the cash: runningCash += profitLossValue; } //Get the annual percentage of profit and loss: foreach (var year in years) { annualNetProfit[year] = (annualWinTotal[year] + annualLossTotal[year]); } //Sum the totals: try { if (profitLoss.Keys.Count > 0) { totalClosedTrades = annualTrades.Values.Sum(); totalWins = annualWins.Values.Sum(); totalLosses = annualLosses.Values.Sum(); totalNetProfit = (equity.Values.LastOrDefault() / startingCash) - 1; //-> Handle Div/0 Errors if (totalWins == 0) { averageWin = 0; } else { averageWin = annualWinTotal.Values.Sum() / totalWins; } if (totalLosses == 0) { averageLoss = 0; averageWinRatio = 0; } else { averageLoss = annualLossTotal.Values.Sum() / totalLosses; averageWinRatio = Math.Abs(averageWin / averageLoss); } if (totalTrades == 0) { winRate = 0; lossRate = 0; } else { winRate = Math.Round(totalWins / totalClosedTrades, 5); lossRate = Math.Round(totalLosses / totalClosedTrades, 5); } } } catch (Exception err) { Log.Error(err, "Second Half:"); } var profitLossRatio = ProfitLossRatio(averageWin, averageLoss); var profitLossRatioHuman = profitLossRatio.ToString(CultureInfo.InvariantCulture); if (profitLossRatio == -1) profitLossRatioHuman = "0"; //Add the over all results first, break down by year later: statistics = new Dictionary { { "Total Trades", Math.Round(totalTrades, 0).ToString(CultureInfo.InvariantCulture) }, { "Average Win", Math.Round(averageWin * 100, 2) + "%" }, { "Average Loss", Math.Round(averageLoss * 100, 2) + "%" }, { "Compounding Annual Return", Math.Round(algoCompoundingPerformance * 100, 3) + "%" }, { "Drawdown", (DrawdownPercent(equity, 3) * 100) + "%" }, { "Expectancy", Math.Round((winRate * averageWinRatio) - (lossRate), 3).ToString(CultureInfo.InvariantCulture) }, { "Net Profit", Math.Round(totalNetProfit * 100, 3) + "%"}, { "Sharpe Ratio", Math.Round(SharpeRatio(listPerformance, riskFreeRate), 3).ToString(CultureInfo.InvariantCulture) }, { "Loss Rate", Math.Round(lossRate * 100) + "%" }, { "Win Rate", Math.Round(winRate * 100) + "%" }, { "Profit-Loss Ratio", profitLossRatioHuman }, { "Alpha", Math.Round(Alpha(listPerformance, listBenchmark, riskFreeRate), 3).ToString(CultureInfo.InvariantCulture) }, { "Beta", Math.Round(Beta(listPerformance, listBenchmark), 3).ToString(CultureInfo.InvariantCulture) }, { "Annual Standard Deviation", Math.Round(AnnualStandardDeviation(listPerformance, tradingDaysPerYear), 3).ToString(CultureInfo.InvariantCulture) }, { "Annual Variance", Math.Round(AnnualVariance(listPerformance, tradingDaysPerYear), 3).ToString(CultureInfo.InvariantCulture) }, { "Information Ratio", Math.Round(InformationRatio(listPerformance, listBenchmark), 3).ToString(CultureInfo.InvariantCulture) }, { "Tracking Error", Math.Round(TrackingError(listPerformance, listBenchmark), 3).ToString(CultureInfo.InvariantCulture) }, { "Treynor Ratio", Math.Round(TreynorRatio(listPerformance, listBenchmark, riskFreeRate), 3).ToString(CultureInfo.InvariantCulture) }, { "Total Fees", "$" + totalFees.ToString("0.00") } }; } catch (Exception err) { Log.Error(err); } return statistics; } /// /// Return profit loss ratio safely avoiding divide by zero errors. /// /// /// /// public static decimal ProfitLossRatio(decimal averageWin, decimal averageLoss) { if (averageLoss == 0) return -1; return Math.Round(averageWin / Math.Abs(averageLoss), 2); } /// /// Drawdown maximum percentage. /// /// /// /// public static decimal DrawdownPercent(SortedDictionary equityOverTime, int rounding = 2) { var dd = 0m; try { var lPrices = equityOverTime.Values.ToList(); var lDrawdowns = new List(); var high = lPrices[0]; foreach (var price in lPrices) { if (price >= high) high = price; lDrawdowns.Add((price/high) - 1); } dd = Math.Round(Math.Abs(lDrawdowns.Min()), rounding); } catch (Exception err) { Log.Error(err); } return dd; } /// /// Drawdown maximum value /// /// Array of portfolio value over time. /// Round the drawdown statistics. /// Draw down percentage over period. public static decimal DrawdownValue(SortedDictionary equityOverTime, int rounding = 2) { //Initialise: var priceMaximum = 0; var previousMinimum = 0; var previousMaximum = 0; try { var lPrices = equityOverTime.Values.ToList(); for (var id = 0; id < lPrices.Count; id++) { if (lPrices[id] >= lPrices[priceMaximum]) { priceMaximum = id; } else { if ((lPrices[priceMaximum] - lPrices[id]) > (lPrices[previousMaximum] - lPrices[previousMinimum])) { previousMaximum = priceMaximum; previousMinimum = id; } } } return Math.Round((lPrices[previousMaximum] - lPrices[previousMinimum]), rounding); } catch (Exception err) { Log.Error(err); } return 0; } // End Drawdown: /// /// Annual compounded returns statistic based on the final-starting capital and years. /// /// Algorithm starting capital /// Algorithm final capital /// Years trading /// Decimal fraction for annual compounding performance public static decimal CompoundingAnnualPerformance(decimal startingCapital, decimal finalCapital, decimal years) { return (years == 0 ? 0d : Math.Pow((double)finalCapital / (double)startingCapital, 1 / (double)years) - 1).SafeDecimalCast(); } /// /// Annualized return statistic calculated as an average of daily trading performance multiplied by the number of trading days per year. /// /// Dictionary collection of double performance values /// Trading days per year for the assets in portfolio /// May be unaccurate for forex algorithms with more trading days in a year /// Double annual performance percentage public static double AnnualPerformance(List performance, double tradingDaysPerYear = 252) { return performance.Average() * tradingDaysPerYear; } /// /// Annualized variance statistic calculation using the daily performance variance and trading days per year. /// /// /// /// Invokes the variance extension in the MathNet Statistics class /// Annual variance value public static double AnnualVariance(List performance, double tradingDaysPerYear = 252) { return (performance.Variance())*tradingDaysPerYear; } /// /// Annualized standard deviation /// /// Collection of double values for daily performance /// Number of trading days for the assets in portfolio to get annualize standard deviation. /// /// Invokes the variance extension in the MathNet Statistics class. /// Feasibly the trading days per year can be fetched from the dictionary of performance which includes the date-times to get the range; if is more than 1 year data. /// /// Value for annual standard deviation public static double AnnualStandardDeviation(List performance, double tradingDaysPerYear = 252) { return Math.Sqrt(performance.Variance() * tradingDaysPerYear); } /// /// Algorithm "beta" statistic - the covariance between the algorithm and benchmark performance, divided by benchmark's variance /// /// Collection of double values for algorithm daily performance. /// Collection of double benchmark daily performance values. /// Invokes the variance and covariance extensions in the MathNet Statistics class /// Value for beta public static double Beta(List algoPerformance, List benchmarkPerformance) { return algoPerformance.Covariance(benchmarkPerformance) / benchmarkPerformance.Variance(); } /// /// Algorithm "Alpha" statistic - abnormal returns over the risk free rate and the relationshio (beta) with the benchmark returns. /// /// Collection of double algorithm daily performance values. /// Collection of double benchmark daily performance values. /// Risk free rate of return for the T-Bonds. /// Value for alpha public static double Alpha(List algoPerformance, List benchmarkPerformance, double riskFreeRate) { return AnnualPerformance(algoPerformance) - (riskFreeRate + Beta(algoPerformance, benchmarkPerformance) * (AnnualPerformance(benchmarkPerformance) - riskFreeRate)); } /// /// Tracking error volatility (TEV) statistic - a measure of how closely a portfolio follows the index to which it is benchmarked /// /// If algo = benchmark, TEV = 0 /// Double collection of algorithm daily performance values /// Double collection of benchmark daily performance values /// Value for tracking error public static double TrackingError(List algoPerformance, List benchmarkPerformance) { return Math.Sqrt(AnnualVariance(algoPerformance) - 2 * Correlation.Pearson(algoPerformance, benchmarkPerformance) * AnnualStandardDeviation(algoPerformance) * AnnualStandardDeviation(benchmarkPerformance) + AnnualVariance(benchmarkPerformance)); } /// /// Information ratio - risk adjusted return /// /// Collection of doubles for the daily algorithm daily performance /// Collection of doubles for the benchmark daily performance /// (risk = tracking error volatility, a volatility measures that considers the volatility of both algo and benchmark) /// /// Value for information ratio public static double InformationRatio(List algoPerformance, List benchmarkPerformance) { return (AnnualPerformance(algoPerformance) - AnnualPerformance(benchmarkPerformance)) / (TrackingError(algoPerformance, benchmarkPerformance)); } /// /// Sharpe ratio with respect to risk free rate: measures excess of return per unit of risk. /// /// With risk defined as the algorithm's volatility /// Collection of double values for the algorithm daily performance /// /// Value for sharpe ratio public static double SharpeRatio(List algoPerformance, double riskFreeRate) { return (AnnualPerformance(algoPerformance) - riskFreeRate) / (AnnualStandardDeviation(algoPerformance)); } /// /// Treynor ratio statistic is a measurement of the returns earned in excess of that which could have been earned on an investment that has no diversifiable risk /// /// Collection of double algorithm daily performance values /// Collection of double benchmark daily performance values /// Risk free rate of return /// double Treynor ratio public static double TreynorRatio(List algoPerformance, List benchmarkPerformance, double riskFreeRate) { return (AnnualPerformance(algoPerformance) - riskFreeRate) / (Beta(algoPerformance, benchmarkPerformance)); } } // End of Statistics } // End of Namespace