17 using System.Collections.Generic;
19 using System.Runtime.CompilerServices;
53 SortedDictionary<DateTime, decimal> profitLoss,
54 List<ISeriesPoint> pointsEquity,
55 List<ISeriesPoint> pointsPerformance,
56 List<ISeriesPoint> pointsBenchmark,
57 List<ISeriesPoint> pointsPortfolioTurnover,
58 decimal startingCapital,
62 string accountCurrencySymbol,
65 int tradingDaysPerYear)
67 var equity = ChartPointToDictionary(pointsEquity);
69 var firstDate = equity.Keys.FirstOrDefault().Date;
70 var lastDate = equity.Keys.LastOrDefault().Date;
72 var totalPerformance = GetAlgorithmPerformance(firstDate, lastDate, trades, profitLoss, equity, pointsPerformance, pointsBenchmark,
73 pointsPortfolioTurnover, startingCapital, transactions, riskFreeInterestRateModel, tradingDaysPerYear);
74 var rollingPerformances = GetRollingPerformances(firstDate, lastDate, trades, profitLoss, equity, pointsPerformance, pointsBenchmark,
75 pointsPortfolioTurnover, startingCapital, transactions, riskFreeInterestRateModel, tradingDaysPerYear);
76 var summary = GetSummary(totalPerformance, estimatedStrategyCapacity, totalFees, totalOrders, accountCurrencySymbol);
103 SortedDictionary<DateTime, decimal> profitLoss,
104 SortedDictionary<DateTime, decimal> equity,
105 List<ISeriesPoint> pointsPerformance,
106 List<ISeriesPoint> pointsBenchmark,
107 List<ISeriesPoint> pointsPortfolioTurnover,
108 decimal startingCapital,
111 int tradingDaysPerYear)
113 var periodEquity =
new SortedDictionary<DateTime, decimal>(equity.Where(x => x.Key.Date >= fromDate && x.Key.Date < toDate.AddDays(1)).ToDictionary(x => x.Key, y => y.Value));
116 if (periodEquity.IsNullOrEmpty())
121 var periodTrades = trades.Where(x => x.ExitTime.Date >= fromDate && x.ExitTime < toDate.AddDays(1)).ToList();
122 var periodProfitLoss =
new SortedDictionary<DateTime, decimal>(profitLoss.Where(x => x.Key >= fromDate && x.Key.Date < toDate.AddDays(1)).ToDictionary(x => x.Key, y => y.Value));
123 var periodWinCount = transactions.
WinningTransactions.Count(x => x.Key >= fromDate && x.Key.Date < toDate.AddDays(1));
124 var periodLossCount = transactions.
LosingTransactions.Count(x => x.Key >= fromDate && x.Key.Date < toDate.AddDays(1));
128 var benchmark = ChartPointToDictionary(pointsBenchmark, fromDate, toDate);
129 var performance = ChartPointToDictionary(pointsPerformance, fromDate, toDate);
130 var portfolioTurnover = ChartPointToDictionary(pointsPortfolioTurnover, fromDate, toDate);
133 if (benchmark.Count != performance.Count)
135 throw new ArgumentException($
"Benchmark and performance series has {Math.Abs(benchmark.Count - performance.Count)} misaligned values.");
142 var listBenchmark = benchmarkEnumerable.Select(x => x.Value).ToList();
145 var runningCapital = equity.Count == periodEquity.Count ? startingCapital : periodEquity.Values.FirstOrDefault();
147 return new AlgorithmPerformance(periodTrades, periodProfitLoss, periodEquity, portfolioTurnover, listPerformance, listBenchmark,
148 runningCapital, periodWinCount, periodLossCount, riskFreeInterestRateModel, tradingDaysPerYear);
169 private static Dictionary<string, AlgorithmPerformance> GetRollingPerformances(
173 SortedDictionary<DateTime, decimal> profitLoss,
174 SortedDictionary<DateTime, decimal> equity,
175 List<ISeriesPoint> pointsPerformance,
176 List<ISeriesPoint> pointsBenchmark,
177 List<ISeriesPoint> pointsPortfolioTurnover,
178 decimal startingCapital,
181 int tradingDaysPerYear)
183 var rollingPerformances =
new Dictionary<string, AlgorithmPerformance>();
185 var monthPeriods =
new[] { 1, 3, 6, 12 };
186 foreach (var monthPeriod
in monthPeriods)
188 var ranges = GetPeriodRanges(monthPeriod, firstDate, lastDate);
190 foreach (var period
in ranges)
192 var key = $
"M{monthPeriod}_{period.EndDate.ToStringInvariant("yyyyMMdd
")}";
193 var periodPerformance = GetAlgorithmPerformance(period.StartDate, period.EndDate, trades, profitLoss, equity, pointsPerformance,
194 pointsBenchmark, pointsPortfolioTurnover, startingCapital, transactions, riskFreeInterestRateModel, tradingDaysPerYear);
195 rollingPerformances[key] = periodPerformance;
199 return rollingPerformances;
205 private static Dictionary<string, string> GetSummary(AlgorithmPerformance totalPerformance, CapacityEstimate estimatedStrategyCapacity,
206 decimal totalFees,
int totalOrders,
string accountCurrencySymbol)
209 var lowestCapacitySymbol = Symbol.Empty;
210 if (estimatedStrategyCapacity !=
null)
212 capacity = estimatedStrategyCapacity.Capacity;
213 lowestCapacitySymbol = estimatedStrategyCapacity.LowestCapacityAsset ?? Symbol.Empty;
216 return new Dictionary<string, string>
218 { PerformanceMetrics.TotalOrders, totalOrders.ToStringInvariant() },
219 { PerformanceMetrics.AverageWin, Math.Round(totalPerformance.PortfolioStatistics.AverageWinRate.SafeMultiply100(), 2).ToStringInvariant() +
"%" },
220 { PerformanceMetrics.AverageLoss, Math.Round(totalPerformance.PortfolioStatistics.AverageLossRate.SafeMultiply100(), 2).ToStringInvariant() +
"%" },
221 { PerformanceMetrics.CompoundingAnnualReturn, Math.Round(totalPerformance.PortfolioStatistics.CompoundingAnnualReturn.SafeMultiply100(), 3).ToStringInvariant() +
"%" },
222 { PerformanceMetrics.Drawdown, Math.Round(totalPerformance.PortfolioStatistics.Drawdown.SafeMultiply100(), 3).ToStringInvariant() +
"%" },
223 { PerformanceMetrics.Expectancy, Math.Round(totalPerformance.PortfolioStatistics.Expectancy, 3).ToStringInvariant() },
224 { PerformanceMetrics.StartEquity, Math.Round(totalPerformance.PortfolioStatistics.StartEquity, 2).ToStringInvariant() },
225 { PerformanceMetrics.EndEquity, Math.Round(totalPerformance.PortfolioStatistics.EndEquity, 2).ToStringInvariant() },
226 { PerformanceMetrics.NetProfit, Math.Round(totalPerformance.PortfolioStatistics.TotalNetProfit.SafeMultiply100(), 3).ToStringInvariant() +
"%"},
227 { PerformanceMetrics.SharpeRatio, Math.Round((
double)totalPerformance.PortfolioStatistics.SharpeRatio, 3).ToStringInvariant() },
228 { PerformanceMetrics.SortinoRatio, Math.Round((
double)totalPerformance.PortfolioStatistics.SortinoRatio, 3).ToStringInvariant() },
229 { PerformanceMetrics.ProbabilisticSharpeRatio, Math.Round(totalPerformance.PortfolioStatistics.ProbabilisticSharpeRatio.SafeMultiply100(), 3).ToStringInvariant() +
"%"},
230 { PerformanceMetrics.LossRate, Math.Round(totalPerformance.PortfolioStatistics.LossRate.SafeMultiply100()).ToStringInvariant() +
"%" },
231 { PerformanceMetrics.WinRate, Math.Round(totalPerformance.PortfolioStatistics.WinRate.SafeMultiply100()).ToStringInvariant() +
"%" },
232 { PerformanceMetrics.ProfitLossRatio, Math.Round(totalPerformance.PortfolioStatistics.ProfitLossRatio, 2).ToStringInvariant() },
233 { PerformanceMetrics.Alpha, Math.Round((
double)totalPerformance.PortfolioStatistics.Alpha, 3).ToStringInvariant() },
234 { PerformanceMetrics.Beta, Math.Round((
double)totalPerformance.PortfolioStatistics.Beta, 3).ToStringInvariant() },
235 { PerformanceMetrics.AnnualStandardDeviation, Math.Round((
double)totalPerformance.PortfolioStatistics.AnnualStandardDeviation, 3).ToStringInvariant() },
236 { PerformanceMetrics.AnnualVariance, Math.Round((
double)totalPerformance.PortfolioStatistics.AnnualVariance, 3).ToStringInvariant() },
237 { PerformanceMetrics.InformationRatio, Math.Round((
double)totalPerformance.PortfolioStatistics.InformationRatio, 3).ToStringInvariant() },
238 { PerformanceMetrics.TrackingError, Math.Round((
double)totalPerformance.PortfolioStatistics.TrackingError, 3).ToStringInvariant() },
239 { PerformanceMetrics.TreynorRatio, Math.Round((
double)totalPerformance.PortfolioStatistics.TreynorRatio, 3).ToStringInvariant() },
240 { PerformanceMetrics.TotalFees, accountCurrencySymbol + totalFees.ToStringInvariant(
"0.00") },
241 { PerformanceMetrics.EstimatedStrategyCapacity, accountCurrencySymbol + capacity.RoundToSignificantDigits(2).ToStringInvariant() },
242 { PerformanceMetrics.LowestCapacityAsset, lowestCapacitySymbol != Symbol.Empty ? lowestCapacitySymbol.ID.ToString() :
"" },
243 { PerformanceMetrics.PortfolioTurnover, Math.Round(totalPerformance.PortfolioStatistics.PortfolioTurnover.SafeMultiply100(), 2).ToStringInvariant() +
"%" }
250 private class PeriodRange
252 internal DateTime StartDate {
get;
set; }
253 internal DateTime EndDate {
get;
set; }
264 private static IEnumerable<PeriodRange> GetPeriodRanges(
int periodMonths, DateTime firstDate, DateTime lastDate)
267 var date = lastDate.Date;
268 var endDates =
new List<DateTime>();
272 date =
new DateTime(date.Year, date.Month, 1).AddDays(-1);
273 }
while (date >= firstDate);
276 var ranges =
new List<PeriodRange> {
new PeriodRange { StartDate = firstDate, EndDate = endDates[endDates.Count - 1] } };
277 for (var i = endDates.Count - 2; i >= 0; i--)
279 var startDate = ranges[ranges.Count - 1].EndDate.AddDays(1).AddMonths(1 - periodMonths);
280 if (startDate < firstDate) startDate = firstDate;
282 ranges.Add(
new PeriodRange
284 StartDate = startDate,
285 EndDate = endDates[i]
300 private static SortedDictionary<DateTime, decimal> ChartPointToDictionary(IEnumerable<ISeriesPoint> points, DateTime? fromDate =
null, DateTime? toDate =
null)
302 var dictionary =
new SortedDictionary<DateTime, decimal>();
304 foreach (var point
in points)
306 if (fromDate !=
null && point.Time.Date < fromDate)
continue;
307 if (toDate !=
null && point.Time.Date >= ((DateTime)toDate).AddDays(1))
break;
309 dictionary[point.Time] = GetPointValue(point);
318 [MethodImpl(MethodImplOptions.AggressiveInlining)]
319 private static decimal GetPointValue(ISeriesPoint point)
321 if (point is ChartPoint)
323 return ((ChartPoint)point).y.Value;
326 return ((Candlestick)point).Close.Value;
336 public static IEnumerable<KeyValuePair<DateTime, double>>
CreateBenchmarkDifferences(IEnumerable<KeyValuePair<DateTime, decimal>> points, DateTime fromDate, DateTime toDate)
338 DateTime dtPrevious =
default;
340 var firstValueSkipped =
false;
341 double deltaPercentage;
344 foreach (var kvp
in points.Where(kvp => kvp.Key >= fromDate.Date && kvp.Key.Date <= toDate))
347 var value = kvp.Value;
349 if (dtPrevious !=
default)
354 deltaPercentage = (double)((value - previous) / previous);
359 if (firstValueSkipped)
361 yield
return new KeyValuePair<DateTime, double>(dt, deltaPercentage);
365 firstValueSkipped =
true;
383 foreach (var kvp
in points.Skip(2))
385 yield
return new KeyValuePair<DateTime, double>(kvp.Key, (
double)(kvp.Value / 100));