/*
* 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");
}
}
}
}