/* * 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.Linq; using QuantConnect.Logging; namespace QuantConnect.Statistics { /// /// The class creates summary and rolling statistics from trades, equity and benchmark points /// public static class StatisticsBuilder { /// /// Generates the statistics and returns the results /// /// The list of closed trades /// Trade record of profits and losses /// The list of daily equity values /// The list of algorithm performance values /// The list of benchmark values /// The algorithm starting capital /// The total fees /// The total number of transactions /// Returns a object public static StatisticsResults Generate( List trades, SortedDictionary profitLoss, List pointsEquity, List pointsPerformance, List pointsBenchmark, decimal startingCapital, decimal totalFees, int totalTransactions) { var equity = ChartPointToDictionary(pointsEquity); var firstDate = equity.Keys.FirstOrDefault().Date; var lastDate = equity.Keys.LastOrDefault().Date; var totalPerformance = GetAlgorithmPerformance(firstDate, lastDate, trades, profitLoss, equity, pointsPerformance, pointsBenchmark, startingCapital); var rollingPerformances = GetRollingPerformances(firstDate, lastDate, trades, profitLoss, equity, pointsPerformance, pointsBenchmark, startingCapital); var summary = GetSummary(totalPerformance, totalFees, totalTransactions); return new StatisticsResults(totalPerformance, rollingPerformances, summary); } /// /// Returns the performance of the algorithm in the specified date range /// /// The initial date of the range /// The final date of the range /// The list of closed trades /// Trade record of profits and losses /// The list of daily equity values /// The list of algorithm performance values /// The list of benchmark values /// The algorithm starting capital /// The algorithm performance private static AlgorithmPerformance GetAlgorithmPerformance( DateTime fromDate, DateTime toDate, List trades, SortedDictionary profitLoss, SortedDictionary equity, List pointsPerformance, List pointsBenchmark, decimal startingCapital) { var periodTrades = trades.Where(x => x.ExitTime.Date >= fromDate && x.ExitTime < toDate.AddDays(1)).ToList(); var periodProfitLoss = new SortedDictionary(profitLoss.Where(x => x.Key >= fromDate && x.Key.Date < toDate.AddDays(1)).ToDictionary(x => x.Key, y => y.Value)); var periodEquity = new SortedDictionary(equity.Where(x => x.Key.Date >= fromDate && x.Key.Date < toDate.AddDays(1)).ToDictionary(x => x.Key, y => y.Value)); var benchmark = ChartPointToDictionary(pointsBenchmark, fromDate, toDate); var performance = ChartPointToDictionary(pointsPerformance, fromDate, toDate); // we need to have the same dates in the performance and benchmark dictionaries, // so we add missing dates with zero value var missingPerformanceDates = benchmark.Keys.Where(x => !performance.ContainsKey(x)); foreach (var date in missingPerformanceDates) { performance.Add(date, 0m); } var listPerformance = new List(); performance.Values.ToList().ForEach(i => listPerformance.Add((double)(i / 100))); var listBenchmark = CreateBenchmarkDifferences(benchmark, periodEquity); EnsureSameLength(listPerformance, listBenchmark); var runningCapital = equity.Count == periodEquity.Count ? startingCapital : periodEquity.Values.FirstOrDefault(); return new AlgorithmPerformance(periodTrades, periodProfitLoss, periodEquity, listPerformance, listBenchmark, runningCapital); } /// /// Returns the rolling performances of the algorithm /// /// The first date of the total period /// The last date of the total period /// The list of closed trades /// Trade record of profits and losses /// The list of daily equity values /// The list of algorithm performance values /// The list of benchmark values /// The algorithm starting capital /// A dictionary with the rolling performances private static Dictionary GetRollingPerformances( DateTime firstDate, DateTime lastDate, List trades, SortedDictionary profitLoss, SortedDictionary equity, List pointsPerformance, List pointsBenchmark, decimal startingCapital) { var rollingPerformances = new Dictionary(); var monthPeriods = new[] { 1, 3, 6, 12 }; foreach (var monthPeriod in monthPeriods) { var ranges = GetPeriodRanges(monthPeriod, firstDate, lastDate); foreach (var period in ranges) { var key = "M" + monthPeriod + "_" + period.EndDate.ToString("yyyyMMdd"); var periodPerformance = GetAlgorithmPerformance(period.StartDate, period.EndDate, trades, profitLoss, equity, pointsPerformance, pointsBenchmark, startingCapital); rollingPerformances[key] = periodPerformance; } } return rollingPerformances; } /// /// Returns a summary of the algorithm performance as a dictionary /// private static Dictionary GetSummary(AlgorithmPerformance totalPerformance, decimal totalFees, int totalTransactions) { return new Dictionary { { "Total Trades", totalTransactions.ToString(CultureInfo.InvariantCulture) }, { "Average Win", Math.Round(totalPerformance.PortfolioStatistics.AverageWinRate.SafeMultiply100(), 2).ToString(CultureInfo.InvariantCulture) + "%" }, { "Average Loss", Math.Round(totalPerformance.PortfolioStatistics.AverageLossRate.SafeMultiply100(), 2).ToString(CultureInfo.InvariantCulture) + "%" }, { "Compounding Annual Return", Math.Round(totalPerformance.PortfolioStatistics.CompoundingAnnualReturn.SafeMultiply100(), 3).ToString(CultureInfo.InvariantCulture) + "%" }, { "Drawdown", Math.Round(totalPerformance.PortfolioStatistics.Drawdown.SafeMultiply100(), 3).ToString(CultureInfo.InvariantCulture) + "%" }, { "Expectancy", Math.Round(totalPerformance.PortfolioStatistics.Expectancy, 3).ToString(CultureInfo.InvariantCulture) }, { "Net Profit", Math.Round(totalPerformance.PortfolioStatistics.TotalNetProfit.SafeMultiply100(), 3).ToString(CultureInfo.InvariantCulture) + "%"}, { "Sharpe Ratio", Math.Round((double)totalPerformance.PortfolioStatistics.SharpeRatio, 3).ToString(CultureInfo.InvariantCulture) }, { "Loss Rate", Math.Round(totalPerformance.PortfolioStatistics.LossRate.SafeMultiply100()).ToString(CultureInfo.InvariantCulture) + "%" }, { "Win Rate", Math.Round(totalPerformance.PortfolioStatistics.WinRate.SafeMultiply100()).ToString(CultureInfo.InvariantCulture) + "%" }, { "Profit-Loss Ratio", Math.Round(totalPerformance.PortfolioStatistics.ProfitLossRatio, 2).ToString(CultureInfo.InvariantCulture) }, { "Alpha", Math.Round((double)totalPerformance.PortfolioStatistics.Alpha, 3).ToString(CultureInfo.InvariantCulture) }, { "Beta", Math.Round((double)totalPerformance.PortfolioStatistics.Beta, 3).ToString(CultureInfo.InvariantCulture) }, { "Annual Standard Deviation", Math.Round((double)totalPerformance.PortfolioStatistics.AnnualStandardDeviation, 3).ToString(CultureInfo.InvariantCulture) }, { "Annual Variance", Math.Round((double)totalPerformance.PortfolioStatistics.AnnualVariance, 3).ToString(CultureInfo.InvariantCulture) }, { "Information Ratio", Math.Round((double)totalPerformance.PortfolioStatistics.InformationRatio, 3).ToString(CultureInfo.InvariantCulture) }, { "Tracking Error", Math.Round((double)totalPerformance.PortfolioStatistics.TrackingError, 3).ToString(CultureInfo.InvariantCulture) }, { "Treynor Ratio", Math.Round((double)totalPerformance.PortfolioStatistics.TreynorRatio, 3).ToString(CultureInfo.InvariantCulture) }, { "Total Fees", "$" + totalFees.ToString("0.00", CultureInfo.InvariantCulture) } }; } private static decimal SafeMultiply100(this decimal value) { const decimal max = decimal.MaxValue/100m; if (value >= max) return decimal.MaxValue; return value*100m; } /// /// Helper class for rolling statistics /// private class PeriodRange { internal DateTime StartDate { get; set; } internal DateTime EndDate { get; set; } } // /// /// Gets a list of date ranges for the requested monthly period /// /// The first and last ranges created are partial periods /// The number of months in the period (valid inputs are [1, 3, 6, 12]) /// The first date of the total period /// The last date of the total period /// The list of date ranges private static IEnumerable GetPeriodRanges(int periodMonths, DateTime firstDate, DateTime lastDate) { // get end dates var date = lastDate.Date; var endDates = new List(); do { endDates.Add(date); date = new DateTime(date.Year, date.Month, 1).AddDays(-1); } while (date >= firstDate); // build period ranges var ranges = new List { new PeriodRange { StartDate = firstDate, EndDate = endDates[endDates.Count - 1] } }; for (var i = endDates.Count - 2; i >= 0; i--) { var startDate = ranges[ranges.Count - 1].EndDate.AddDays(1).AddMonths(1 - periodMonths); if (startDate < firstDate) startDate = firstDate; ranges.Add(new PeriodRange { StartDate = startDate, EndDate = endDates[i] }); } return ranges; } /// /// 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 /// An optional starting date /// An optional ending date /// SortedDictionary of the equity decimal values ordered in time private static SortedDictionary ChartPointToDictionary(IEnumerable points, DateTime? fromDate = null, DateTime? toDate = null) { var dictionary = new SortedDictionary(); foreach (var point in points) { var x = Time.UnixTimeStampToDateTime(point.x); if (fromDate != null && x.Date < fromDate) continue; if (toDate != null && x.Date >= ((DateTime)toDate).AddDays(1)) break; dictionary[x] = point.y; } return dictionary; } /// /// Creates a list of benchmark differences for the period /// /// The benchmark values /// The equity values /// The list of benchmark differences private static List CreateBenchmarkDifferences(SortedDictionary benchmark, SortedDictionary equity) { // 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 start looking for data in a inexistent day. // If running a short backtest this will skew results, longer backtests will not be affected much var dtPrevious = new DateTime(); var listBenchmark = new List(); var minDate = equity.Keys.FirstOrDefault().AddDays(-1); var maxDate = equity.Keys.LastOrDefault(); // Get benchmark performance array for same period: benchmark.Keys.ToList().ForEach(dt => { if (dt >= minDate && dt <= maxDate) { 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; } }); return listBenchmark; } /// /// Ensures the performance list and benchmark list have the same length, padding with trailing zeros /// /// The performance list /// The benchmark list private static void EnsureSameLength(List listPerformance, List listBenchmark) { // THIS SHOULD NEVER HAPPEN --> But if it does, log it and fail silently. while (listPerformance.Count < listBenchmark.Count) { listPerformance.Add(0); Log.Trace("StatisticsBuilder.EnsureSameLength(): Padded Performance"); } while (listPerformance.Count > listBenchmark.Count) { listBenchmark.Add(0); Log.Trace("StatisticsBuilder.EnsureSameLength(): Padded Benchmark"); } } } }