/*
* 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.Linq;
using System.Runtime.CompilerServices;
using QuantConnect.Data;
using QuantConnect.Securities;
using QuantConnect.Util;
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 list of portfolio turnover daily samples
/// The algorithm starting capital
/// The total fees
/// The total number of transactions
/// The estimated capacity of this strategy
/// The account currency symbol
///
/// The transaction manager to get number of winning and losing transactions
///
/// The risk free interest rate model to use
/// The number of trading days per year
/// Returns a object
public static StatisticsResults Generate(
List trades,
SortedDictionary profitLoss,
List pointsEquity,
List pointsPerformance,
List pointsBenchmark,
List pointsPortfolioTurnover,
decimal startingCapital,
decimal totalFees,
int totalOrders,
CapacityEstimate estimatedStrategyCapacity,
string accountCurrencySymbol,
SecurityTransactionManager transactions,
IRiskFreeInterestRateModel riskFreeInterestRateModel,
int tradingDaysPerYear)
{
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,
pointsPortfolioTurnover, startingCapital, transactions, riskFreeInterestRateModel, tradingDaysPerYear);
var rollingPerformances = GetRollingPerformances(firstDate, lastDate, trades, profitLoss, equity, pointsPerformance, pointsBenchmark,
pointsPortfolioTurnover, startingCapital, transactions, riskFreeInterestRateModel, tradingDaysPerYear);
var summary = GetSummary(totalPerformance, estimatedStrategyCapacity, totalFees, totalOrders, accountCurrencySymbol);
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 list of portfolio turnover daily samples
/// The algorithm starting capital
///
/// The transaction manager to get number of winning and losing transactions
///
/// The risk free interest rate model to use
/// The number of trading days per year
/// The algorithm performance
private static AlgorithmPerformance GetAlgorithmPerformance(
DateTime fromDate,
DateTime toDate,
List trades,
SortedDictionary profitLoss,
SortedDictionary equity,
List pointsPerformance,
List pointsBenchmark,
List pointsPortfolioTurnover,
decimal startingCapital,
SecurityTransactionManager transactions,
IRiskFreeInterestRateModel riskFreeInterestRateModel,
int tradingDaysPerYear)
{
var periodEquity = new SortedDictionary(equity.Where(x => x.Key.Date >= fromDate && x.Key.Date < toDate.AddDays(1)).ToDictionary(x => x.Key, y => y.Value));
// No portfolio equity for the period means that there is no performance to be computed
if (periodEquity.IsNullOrEmpty())
{
return new AlgorithmPerformance();
}
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 periodWinCount = transactions.WinningTransactions.Count(x => x.Key >= fromDate && x.Key.Date < toDate.AddDays(1));
var periodLossCount = transactions.LosingTransactions.Count(x => x.Key >= fromDate && x.Key.Date < toDate.AddDays(1));
// Convert our charts to dictionaries
// NOTE: Day 0 refers to sample taken at 12AM on StartDate, performance[0] always = 0, benchmark[0] is benchmark value preceding start date.
var benchmark = ChartPointToDictionary(pointsBenchmark, fromDate, toDate);
var performance = ChartPointToDictionary(pointsPerformance, fromDate, toDate);
var portfolioTurnover = ChartPointToDictionary(pointsPortfolioTurnover, fromDate, toDate);
// Ensure our series are aligned
if (benchmark.Count != performance.Count)
{
throw new ArgumentException($"Benchmark and performance series has {Math.Abs(benchmark.Count - performance.Count)} misaligned values.");
}
// Convert our benchmark values into a percentage daily performance of the benchmark, this will shorten the series by one since
// its the percentage change between each entry (No day 0 sample)
var benchmarkEnumerable = CreateBenchmarkDifferences(benchmark, fromDate, toDate);
var listBenchmark = benchmarkEnumerable.Select(x => x.Value).ToList();
var listPerformance = PreprocessPerformanceValues(performance).Select(x => x.Value).ToList();
var runningCapital = equity.Count == periodEquity.Count ? startingCapital : periodEquity.Values.FirstOrDefault();
return new AlgorithmPerformance(periodTrades, periodProfitLoss, periodEquity, portfolioTurnover, listPerformance, listBenchmark,
runningCapital, periodWinCount, periodLossCount, riskFreeInterestRateModel, tradingDaysPerYear);
}
///
/// 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 list of portfolio turnover daily samples
/// The algorithm starting capital
///
/// The transaction manager to get number of winning and losing transactions
///
/// The risk free interest rate model to use
/// The number of trading days per year
/// A dictionary with the rolling performances
private static Dictionary GetRollingPerformances(
DateTime firstDate,
DateTime lastDate,
List trades,
SortedDictionary profitLoss,
SortedDictionary equity,
List pointsPerformance,
List pointsBenchmark,
List pointsPortfolioTurnover,
decimal startingCapital,
SecurityTransactionManager transactions,
IRiskFreeInterestRateModel riskFreeInterestRateModel,
int tradingDaysPerYear)
{
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.ToStringInvariant("yyyyMMdd")}";
var periodPerformance = GetAlgorithmPerformance(period.StartDate, period.EndDate, trades, profitLoss, equity, pointsPerformance,
pointsBenchmark, pointsPortfolioTurnover, startingCapital, transactions, riskFreeInterestRateModel, tradingDaysPerYear);
rollingPerformances[key] = periodPerformance;
}
}
return rollingPerformances;
}
///
/// Returns a summary of the algorithm performance as a dictionary
///
private static Dictionary GetSummary(AlgorithmPerformance totalPerformance, CapacityEstimate estimatedStrategyCapacity,
decimal totalFees, int totalOrders, string accountCurrencySymbol)
{
var capacity = 0m;
var lowestCapacitySymbol = Symbol.Empty;
if (estimatedStrategyCapacity != null)
{
capacity = estimatedStrategyCapacity.Capacity;
lowestCapacitySymbol = estimatedStrategyCapacity.LowestCapacityAsset ?? Symbol.Empty;
}
return new Dictionary
{
{ PerformanceMetrics.TotalOrders, totalOrders.ToStringInvariant() },
{ PerformanceMetrics.AverageWin, Math.Round(totalPerformance.PortfolioStatistics.AverageWinRate.SafeMultiply100(), 2).ToStringInvariant() + "%" },
{ PerformanceMetrics.AverageLoss, Math.Round(totalPerformance.PortfolioStatistics.AverageLossRate.SafeMultiply100(), 2).ToStringInvariant() + "%" },
{ PerformanceMetrics.CompoundingAnnualReturn, Math.Round(totalPerformance.PortfolioStatistics.CompoundingAnnualReturn.SafeMultiply100(), 3).ToStringInvariant() + "%" },
{ PerformanceMetrics.Drawdown, Math.Round(totalPerformance.PortfolioStatistics.Drawdown.SafeMultiply100(), 3).ToStringInvariant() + "%" },
{ PerformanceMetrics.Expectancy, Math.Round(totalPerformance.PortfolioStatistics.Expectancy, 3).ToStringInvariant() },
{ PerformanceMetrics.StartEquity, Math.Round(totalPerformance.PortfolioStatistics.StartEquity, 2).ToStringInvariant() },
{ PerformanceMetrics.EndEquity, Math.Round(totalPerformance.PortfolioStatistics.EndEquity, 2).ToStringInvariant() },
{ PerformanceMetrics.NetProfit, Math.Round(totalPerformance.PortfolioStatistics.TotalNetProfit.SafeMultiply100(), 3).ToStringInvariant() + "%"},
{ PerformanceMetrics.SharpeRatio, Math.Round((double)totalPerformance.PortfolioStatistics.SharpeRatio, 3).ToStringInvariant() },
{ PerformanceMetrics.SortinoRatio, Math.Round((double)totalPerformance.PortfolioStatistics.SortinoRatio, 3).ToStringInvariant() },
{ PerformanceMetrics.ProbabilisticSharpeRatio, Math.Round(totalPerformance.PortfolioStatistics.ProbabilisticSharpeRatio.SafeMultiply100(), 3).ToStringInvariant() + "%"},
{ PerformanceMetrics.LossRate, Math.Round(totalPerformance.PortfolioStatistics.LossRate.SafeMultiply100()).ToStringInvariant() + "%" },
{ PerformanceMetrics.WinRate, Math.Round(totalPerformance.PortfolioStatistics.WinRate.SafeMultiply100()).ToStringInvariant() + "%" },
{ PerformanceMetrics.ProfitLossRatio, Math.Round(totalPerformance.PortfolioStatistics.ProfitLossRatio, 2).ToStringInvariant() },
{ PerformanceMetrics.Alpha, Math.Round((double)totalPerformance.PortfolioStatistics.Alpha, 3).ToStringInvariant() },
{ PerformanceMetrics.Beta, Math.Round((double)totalPerformance.PortfolioStatistics.Beta, 3).ToStringInvariant() },
{ PerformanceMetrics.AnnualStandardDeviation, Math.Round((double)totalPerformance.PortfolioStatistics.AnnualStandardDeviation, 3).ToStringInvariant() },
{ PerformanceMetrics.AnnualVariance, Math.Round((double)totalPerformance.PortfolioStatistics.AnnualVariance, 3).ToStringInvariant() },
{ PerformanceMetrics.InformationRatio, Math.Round((double)totalPerformance.PortfolioStatistics.InformationRatio, 3).ToStringInvariant() },
{ PerformanceMetrics.TrackingError, Math.Round((double)totalPerformance.PortfolioStatistics.TrackingError, 3).ToStringInvariant() },
{ PerformanceMetrics.TreynorRatio, Math.Round((double)totalPerformance.PortfolioStatistics.TreynorRatio, 3).ToStringInvariant() },
{ PerformanceMetrics.TotalFees, accountCurrencySymbol + totalFees.ToStringInvariant("0.00") },
{ PerformanceMetrics.EstimatedStrategyCapacity, accountCurrencySymbol + capacity.RoundToSignificantDigits(2).ToStringInvariant() },
{ PerformanceMetrics.LowestCapacityAsset, lowestCapacitySymbol != Symbol.Empty ? lowestCapacitySymbol.ID.ToString() : "" },
{ PerformanceMetrics.PortfolioTurnover, Math.Round(totalPerformance.PortfolioStatistics.PortfolioTurnover.SafeMultiply100(), 2).ToStringInvariant() + "%" },
{ PerformanceMetrics.DrawdownRecovery, totalPerformance.PortfolioStatistics.DrawdownRecovery.ToStringInvariant() },
};
}
///
/// 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)
{
if (fromDate != null && point.Time.Date < fromDate) continue;
if (toDate != null && point.Time.Date >= ((DateTime)toDate).AddDays(1)) break;
dictionary[point.Time] = GetPointValue(point);
}
return dictionary;
}
///
/// Gets the value of a point, either ChartPoint.y or Candlestick.Close
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static decimal GetPointValue(ISeriesPoint point)
{
if (point is ChartPoint)
{
return ((ChartPoint)point).y.Value;
}
return ((Candlestick)point).Close.Value;
}
///
/// Yields pairs of date and percentage change for the period
///
/// The values to calculate percentage change for
/// Starting date (inclusive)
/// Ending date (inclusive)
/// Pairs of date and percentage change
public static IEnumerable> CreateBenchmarkDifferences(IEnumerable> points, DateTime fromDate, DateTime toDate)
{
DateTime dtPrevious = default;
var previous = 0m;
var firstValueSkipped = false;
double deltaPercentage;
// Get points performance array for the given period:
foreach (var kvp in points.Where(kvp => kvp.Key >= fromDate.Date && kvp.Key.Date <= toDate))
{
var dt = kvp.Key;
var value = kvp.Value;
if (dtPrevious != default)
{
deltaPercentage = 0;
if (previous != 0)
{
deltaPercentage = (double)((value - previous) / previous);
}
// We will skip past day 1 of performance values to deal with the OnOpen orders causing misalignment between benchmark and
// algorithm performance. So we drop the first value of listBenchmark (Day 1), and drop two values from performance (Day 0, Day 1)
if (firstValueSkipped)
{
yield return new KeyValuePair(dt, deltaPercentage);
}
else
{
firstValueSkipped = true;
}
}
dtPrevious = dt;
previous = value;
}
}
///
/// Skips the first two entries from the given points and divides each entry by 100
///
/// The values to divide by 100
/// Pairs of date and performance value divided by 100
public static IEnumerable> PreprocessPerformanceValues(IEnumerable> points)
{
// We will skip past day 1 of performance values to deal with the OnOpen orders causing misalignment between benchmark and
// algorithm performance. So we drop two values from performance (Day 0, Day 1)
foreach (var kvp in points.Skip(2))
{
yield return new KeyValuePair(kvp.Key, (double)(kvp.Value / 100));
}
}
}
}