chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:02:50 +08:00
commit 0fc60fdcb1
5008 changed files with 910633 additions and 0 deletions
+97
View File
@@ -0,0 +1,97 @@
/*
* 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 QuantConnect.Data;
using System;
using System.Collections.Generic;
namespace QuantConnect.Statistics
{
/// <summary>
/// The <see cref="AlgorithmPerformance"/> class is a wrapper for <see cref="TradeStatistics"/> and <see cref="PortfolioStatistics"/>
/// </summary>
public class AlgorithmPerformance
{
/// <summary>
/// The algorithm statistics on closed trades
/// </summary>
public TradeStatistics TradeStatistics { get; set; }
/// <summary>
/// The algorithm statistics on portfolio
/// </summary>
public PortfolioStatistics PortfolioStatistics { get; set; }
/// <summary>
/// The list of closed trades
/// </summary>
public List<Trade> ClosedTrades { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="AlgorithmPerformance"/> class
/// </summary>
/// <param name="trades">The list of closed trades</param>
/// <param name="profitLoss">Trade record of profits and losses</param>
/// <param name="equity">The list of daily equity values</param>
/// <param name="portfolioTurnover">The algorithm portfolio turnover</param>
/// <param name="listPerformance">The list of algorithm performance values</param>
/// <param name="listBenchmark">The list of benchmark values</param>
/// <param name="startingCapital">The algorithm starting capital</param>
/// <param name="winningTransactions">Number of winning transactions</param>
/// <param name="losingTransactions">Number of losing transactions</param>
/// <param name="riskFreeInterestRateModel">The risk free interest rate model to use</param>
/// <param name="tradingDaysPerYear">The number of trading days per year</param>
public AlgorithmPerformance(
List<Trade> trades,
SortedDictionary<DateTime, decimal> profitLoss,
SortedDictionary<DateTime, decimal> equity,
SortedDictionary<DateTime, decimal> portfolioTurnover,
List<double> listPerformance,
List<double> listBenchmark,
decimal startingCapital,
int winningTransactions,
int losingTransactions,
IRiskFreeInterestRateModel riskFreeInterestRateModel,
int tradingDaysPerYear)
{
TradeStatistics = new TradeStatistics(trades);
PortfolioStatistics = new PortfolioStatistics(profitLoss, equity, portfolioTurnover, listPerformance, listBenchmark, startingCapital,
riskFreeInterestRateModel, tradingDaysPerYear, winningTransactions, losingTransactions);
ClosedTrades = trades;
}
/// <summary>
/// Initializes a new instance of the <see cref="AlgorithmPerformance"/> class
/// </summary>
public AlgorithmPerformance()
{
TradeStatistics = new TradeStatistics();
PortfolioStatistics = new PortfolioStatistics();
ClosedTrades = new List<Trade>();
}
/// <summary>
/// Initializes a new instance of the <see cref="AlgorithmPerformance"/> class
/// </summary>
/// <param name="other">The performance instance to use as a base</param>
public AlgorithmPerformance(AlgorithmPerformance other)
{
TradeStatistics = other.TradeStatistics;
PortfolioStatistics = other.PortfolioStatistics;
ClosedTrades = other.ClosedTrades;
}
}
}
+47
View File
@@ -0,0 +1,47 @@
/*
* 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.
*
*/
namespace QuantConnect.Statistics
{
/// <summary>
/// Represents the result of a drawdown analysis, including the maximum drawdown percentage
/// and the maximum recovery time in days.
/// </summary>
public class DrawdownMetrics
{
/// <summary>
/// Gets the maximum drawdown as a positive percentage.
/// </summary>
public decimal Drawdown { get; }
/// <summary>
/// Gets the maximum recovery time in days from peak to full recovery.
/// </summary>
public int DrawdownRecovery { get; }
/// <summary>
/// Initializes a new instance of the <see cref="DrawdownMetrics"/> class
/// with the specified maximum drawdown and recovery time.
/// </summary>
/// <param name="drawdown">The maximum drawdown as a positive percentage.</param>
/// <param name="recoveryTime">The maximum number of days it took to recover from a drawdown.</param>
public DrawdownMetrics(decimal drawdown, int recoveryTime)
{
Drawdown = drawdown;
DrawdownRecovery = recoveryTime;
}
}
}
+39
View File
@@ -0,0 +1,39 @@
/*
* 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.ComponentModel.Composition;
namespace QuantConnect.Statistics
{
/// <summary>
/// This interface exposes methods for accessing algorithm statistics results at runtime.
/// </summary>
public interface IStatisticsService
{
/// <summary>
/// Calculates and gets the current statistics for the algorithm
/// </summary>
/// <returns>The current statistics</returns>
StatisticsResults StatisticsResults();
/// <summary>
/// Sets or updates a custom summary statistic
/// </summary>
/// <param name="name">The statistic name</param>
/// <param name="value">The statistic value</param>
void SetSummaryStatistic(string name, string value);
}
}
+166
View File
@@ -0,0 +1,166 @@
/*
* 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.
*/
namespace QuantConnect.Statistics
{
/// <summary>
/// PerformanceMetrics contains the names of the various performance metrics used for evaluation purposes.
/// </summary>
public static class PerformanceMetrics
{
/// <summary>
/// Algorithm "Alpha" statistic - abnormal returns over the risk free rate and the relationshio (beta) with the benchmark returns.
/// </summary>
public const string Alpha = "Alpha";
/// <summary>
/// Annualized standard deviation
/// </summary>
public const string AnnualStandardDeviation = "Annual Standard Deviation";
/// <summary>
/// Annualized variance statistic calculation using the daily performance variance and trading days per year.
/// </summary>
public const string AnnualVariance = "Annual Variance";
/// <summary>
/// The average rate of return for trades with zero or negative profit loss
/// </summary>
public const string AverageLoss = "Average Loss";
/// <summary>
/// The average rate of return for trades with positive profit loss
/// </summary>
public const string AverageWin = "Average Win";
/// <summary>
/// Algorithm "beta" statistic - the covariance between the algorithm and benchmark performance, divided by benchmark's variance
/// </summary>
public const string Beta = "Beta";
/// <summary>
/// Annual compounded returns statistic based on the final-starting capital and years.
/// </summary>
public const string CompoundingAnnualReturn = "Compounding Annual Return";
/// <summary>
/// Drawdown maximum percentage.
/// </summary>
public const string Drawdown = "Drawdown";
/// <summary>
/// Total capacity of the algorithm
/// </summary>
public const string EstimatedStrategyCapacity = "Estimated Strategy Capacity";
/// <summary>
/// The expected value of the rate of return
/// </summary>
public const string Expectancy = "Expectancy";
/// <summary>
/// Initial Equity Total Value
/// </summary>
public const string StartEquity = "Start Equity";
/// <summary>
/// Final Equity Total Value
/// </summary>
public const string EndEquity = "End Equity";
/// <summary>
/// Information ratio - risk adjusted return
/// </summary>
public const string InformationRatio = "Information Ratio";
/// <summary>
/// The ratio of the number of trades with zero or negative profit loss to the total number of trades
/// </summary>
public const string LossRate = "Loss Rate";
/// <summary>
/// Total net profit percentage
/// </summary>
public const string NetProfit = "Net Profit";
/// <summary>
/// Probabilistic Sharpe Ratio is a probability measure associated with the Sharpe ratio.
/// It informs us of the probability that the estimated Sharpe ratio is greater than a chosen benchmark
/// </summary>
/// <remarks>See https://www.quantconnect.com/forum/discussion/6483/probabilistic-sharpe-ratio/p1</remarks>
public const string ProbabilisticSharpeRatio = "Probabilistic Sharpe Ratio";
/// <summary>
/// The ratio of the average win rate to the average loss rate
/// </summary>
/// <remarks>If the average loss rate is zero, ProfitLossRatio is set to 0</remarks>
public const string ProfitLossRatio = "Profit-Loss Ratio";
/// <summary>
/// Sharpe ratio with respect to risk free rate: measures excess of return per unit of risk.
/// </summary>
/// <remarks>With risk defined as the algorithm's volatility</remarks>
public const string SharpeRatio = "Sharpe Ratio";
/// <summary>
/// Sortino ratio with respect to risk free rate: measures excess of return per unit of downside risk.
/// </summary>
/// <remarks>With risk defined as the algorithm's volatility</remarks>
public const string SortinoRatio = "Sortino Ratio";
/// <summary>
/// Total amount of fees in the account currency
/// </summary>
public const string TotalFees = "Total Fees";
/// <summary>
/// Total amount of orders in the algorithm
/// </summary>
public const string TotalOrders = "Total Orders";
/// <summary>
/// Tracking error volatility (TEV) statistic - a measure of how closely a portfolio follows the index to which it is benchmarked
/// </summary>
/// <remarks>If algo = benchmark, TEV = 0</remarks>
public const string TrackingError = "Tracking Error";
/// <summary>
/// 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
/// </summary>
public const string TreynorRatio = "Treynor Ratio";
/// <summary>
/// The ratio of the number of trades with positive profit loss to the total number of trades
/// </summary>
/// <remarks>If the total number of trades is zero, WinRate is set to zero</remarks>
public const string WinRate = "Win Rate";
/// <summary>
/// Provide a reference to the lowest capacity symbol used in scaling down the capacity for debugging.
/// </summary>
public const string LowestCapacityAsset = "Lowest Capacity Asset";
/// <summary>
/// The average Portfolio Turnover
/// </summary>
public const string PortfolioTurnover = "Portfolio Turnover";
/// <summary>
/// The recovery time of the maximum drawdown.
/// </summary>
public const string DrawdownRecovery = "Drawdown Recovery";
}
}
+376
View File
@@ -0,0 +1,376 @@
/*
* 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 MathNet.Numerics.Distributions;
using MathNet.Numerics.Statistics;
using Newtonsoft.Json;
using QuantConnect.Data;
using QuantConnect.Util;
namespace QuantConnect.Statistics
{
/// <summary>
/// The <see cref="PortfolioStatistics"/> class represents a set of statistics calculated from equity and benchmark samples
/// </summary>
public class PortfolioStatistics
{
/// <summary>
/// The average rate of return for trades with positive profit loss
/// </summary>
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal AverageWinRate { get; set; }
/// <summary>
/// The average rate of return for trades with zero or negative profit loss
/// </summary>
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal AverageLossRate { get; set; }
/// <summary>
/// The ratio of the average win rate to the average loss rate
/// </summary>
/// <remarks>If the average loss rate is zero, ProfitLossRatio is set to 0</remarks>
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal ProfitLossRatio { get; set; }
/// <summary>
/// The ratio of the number of trades with positive profit loss to the total number of trades
/// </summary>
/// <remarks>If the total number of trades is zero, WinRate is set to zero</remarks>
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal WinRate { get; set; }
/// <summary>
/// The ratio of the number of trades with zero or negative profit loss to the total number of trades
/// </summary>
/// <remarks>If the total number of trades is zero, LossRate is set to zero</remarks>
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal LossRate { get; set; }
/// <summary>
/// The expected value of the rate of return
/// </summary>
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal Expectancy { get; set; }
/// <summary>
/// Initial Equity Total Value
/// </summary>
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal StartEquity { get; set; }
/// <summary>
/// Final Equity Total Value
/// </summary>
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal EndEquity { get; set; }
/// <summary>
/// Annual compounded returns statistic based on the final-starting capital and years.
/// </summary>
/// <remarks>Also known as Compound Annual Growth Rate (CAGR)</remarks>
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal CompoundingAnnualReturn { get; set; }
/// <summary>
/// Drawdown maximum percentage.
/// </summary>
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal Drawdown { get; set; }
/// <summary>
/// The total net profit percentage.
/// </summary>
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal TotalNetProfit { get; set; }
/// <summary>
/// Sharpe ratio with respect to risk free rate: measures excess of return per unit of risk.
/// </summary>
/// <remarks>With risk defined as the algorithm's volatility</remarks>
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal SharpeRatio { get; set; }
/// <summary>
/// Probabilistic Sharpe Ratio is a probability measure associated with the Sharpe ratio.
/// It informs us of the probability that the estimated Sharpe ratio is greater than a chosen benchmark
/// </summary>
/// <remarks>See https://www.quantconnect.com/forum/discussion/6483/probabilistic-sharpe-ratio/p1</remarks>
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal ProbabilisticSharpeRatio { get; set; }
/// <summary>
/// Sortino ratio with respect to risk free rate: measures excess of return per unit of downside risk.
/// </summary>
/// <remarks>With risk defined as the algorithm's volatility</remarks>
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal SortinoRatio { get; set; }
/// <summary>
/// Algorithm "Alpha" statistic - abnormal returns over the risk free rate and the relationshio (beta) with the benchmark returns.
/// </summary>
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal Alpha { get; set; }
/// <summary>
/// Algorithm "beta" statistic - the covariance between the algorithm and benchmark performance, divided by benchmark's variance
/// </summary>
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal Beta { get; set; }
/// <summary>
/// Annualized standard deviation
/// </summary>
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal AnnualStandardDeviation { get; set; }
/// <summary>
/// Annualized variance statistic calculation using the daily performance variance and trading days per year.
/// </summary>
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal AnnualVariance { get; set; }
/// <summary>
/// Information ratio - risk adjusted return
/// </summary>
/// <remarks>(risk = tracking error volatility, a volatility measures that considers the volatility of both algo and benchmark)</remarks>
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal InformationRatio { get; set; }
/// <summary>
/// Tracking error volatility (TEV) statistic - a measure of how closely a portfolio follows the index to which it is benchmarked
/// </summary>
/// <remarks>If algo = benchmark, TEV = 0</remarks>
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal TrackingError { get; set; }
/// <summary>
/// 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
/// </summary>
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal TreynorRatio { get; set; }
/// <summary>
/// The average Portfolio Turnover
/// </summary>
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal PortfolioTurnover { get; set; }
/// <summary>
/// The 1-day VaR for the portfolio, using the Variance-covariance approach.
/// Assumes a 99% confidence level, 1 year lookback period, and that the returns are normally distributed.
/// </summary>
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal ValueAtRisk99 { get; set; }
/// <summary>
/// The 1-day VaR for the portfolio, using the Variance-covariance approach.
/// Assumes a 95% confidence level, 1 year lookback period, and that the returns are normally distributed.
/// </summary>
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal ValueAtRisk95 { get; set; }
/// <summary>
/// The recovery time of the maximum drawdown.
/// </summary>
[JsonConverter(typeof(JsonRoundingConverter))]
public int DrawdownRecovery { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="PortfolioStatistics"/> class
/// </summary>
/// <param name="profitLoss">Trade record of profits and losses</param>
/// <param name="equity">The list of daily equity values</param>
/// <param name="portfolioTurnover">The algorithm portfolio turnover</param>
/// <param name="listPerformance">The list of algorithm performance values</param>
/// <param name="listBenchmark">The list of benchmark values</param>
/// <param name="startingCapital">The algorithm starting capital</param>
/// <param name="riskFreeInterestRateModel">The risk free interest rate model to use</param>
/// <param name="tradingDaysPerYear">The number of trading days per year</param>
/// <param name="winCount">
/// The number of wins, including ITM options with profitLoss less than 0.
/// If this and <paramref name="lossCount"/> are null, they will be calculated from <paramref name="profitLoss"/>
/// </param>
/// <param name="lossCount">The number of losses</param>
public PortfolioStatistics(
SortedDictionary<DateTime, decimal> profitLoss,
SortedDictionary<DateTime, decimal> equity,
SortedDictionary<DateTime, decimal> portfolioTurnover,
List<double> listPerformance,
List<double> listBenchmark,
decimal startingCapital,
IRiskFreeInterestRateModel riskFreeInterestRateModel,
int tradingDaysPerYear,
int? winCount = null,
int? lossCount = null)
{
StartEquity = startingCapital;
EndEquity = equity.LastOrDefault().Value;
if (portfolioTurnover.Count > 0)
{
PortfolioTurnover = portfolioTurnover.Select(kvp => kvp.Value).Average();
}
if (startingCapital == 0
// minimum amount of samples to calculate variance
|| listBenchmark.Count < 2
|| listPerformance.Count < 2)
{
return;
}
var runningCapital = startingCapital;
var totalProfit = 0m;
var totalLoss = 0m;
var totalWins = 0;
var totalLosses = 0;
foreach (var pair in profitLoss)
{
var tradeProfitLoss = pair.Value;
if (tradeProfitLoss > 0)
{
totalProfit += tradeProfitLoss / runningCapital;
totalWins++;
}
else
{
totalLoss += tradeProfitLoss / runningCapital;
totalLosses++;
}
runningCapital += tradeProfitLoss;
}
AverageWinRate = totalWins == 0 ? 0 : totalProfit / totalWins;
AverageLossRate = totalLosses == 0 ? 0 : totalLoss / totalLosses;
ProfitLossRatio = AverageLossRate == 0 ? 0 : AverageWinRate / Math.Abs(AverageLossRate);
// Set the actual total wins and losses count.
// Some options assignments (ITM) count as wins even though they are losses.
if (winCount.HasValue && lossCount.HasValue)
{
totalWins = winCount.Value;
totalLosses = lossCount.Value;
}
var totalTrades = totalWins + totalLosses;
WinRate = totalTrades == 0 ? 0 : (decimal)totalWins / totalTrades;
LossRate = totalTrades == 0 ? 0 : (decimal)totalLosses / totalTrades;
Expectancy = WinRate * ProfitLossRatio - LossRate;
if (startingCapital != 0)
{
TotalNetProfit = equity.Values.LastOrDefault() / startingCapital - 1;
}
var fractionOfYears = (decimal)(equity.Keys.LastOrDefault() - equity.Keys.FirstOrDefault()).TotalDays / 365;
CompoundingAnnualReturn = Statistics.CompoundingAnnualPerformance(startingCapital, equity.Values.LastOrDefault(), fractionOfYears);
AnnualVariance = Statistics.AnnualVariance(listPerformance, tradingDaysPerYear).SafeDecimalCast();
AnnualStandardDeviation = (decimal)Math.Sqrt((double)AnnualVariance);
var benchmarkAnnualPerformance = GetAnnualPerformance(listBenchmark, tradingDaysPerYear);
var annualPerformance = GetAnnualPerformance(listPerformance, tradingDaysPerYear);
var riskFreeRate = riskFreeInterestRateModel.GetAverageRiskFreeRate(equity.Select(x => x.Key));
SharpeRatio = AnnualStandardDeviation == 0 ? 0 : Statistics.SharpeRatio(annualPerformance, AnnualStandardDeviation, riskFreeRate);
var annualDownsideDeviation = Statistics.AnnualDownsideStandardDeviation(listPerformance, tradingDaysPerYear).SafeDecimalCast();
SortinoRatio = annualDownsideDeviation == 0 ? 0 : Statistics.SharpeRatio(annualPerformance, annualDownsideDeviation, riskFreeRate);
var benchmarkVariance = listBenchmark.Variance();
Beta = benchmarkVariance.IsNaNOrZero() ? 0 : (decimal)(listPerformance.Covariance(listBenchmark) / benchmarkVariance);
Alpha = Beta == 0 ? 0 : annualPerformance - (riskFreeRate + Beta * (benchmarkAnnualPerformance - riskFreeRate));
TrackingError = (decimal)Statistics.TrackingError(listPerformance, listBenchmark, (double)tradingDaysPerYear);
InformationRatio = TrackingError == 0 ? 0 : Extensions.SafeDecimalCast((double)annualPerformance - (double)benchmarkAnnualPerformance).SafeDivision(TrackingError);
TreynorRatio = Beta == 0 ? 0 : Extensions.SafeDecimalCast((double)annualPerformance - (double)riskFreeRate).SafeDivision(Beta);
// deannualize a 1 sharpe ratio
var benchmarkSharpeRatio = 1.0d / Math.Sqrt(tradingDaysPerYear);
ProbabilisticSharpeRatio = Statistics.ProbabilisticSharpeRatio(listPerformance, benchmarkSharpeRatio, (double)riskFreeRate / tradingDaysPerYear).SafeDecimalCast();
ValueAtRisk99 = GetValueAtRisk(listPerformance, tradingDaysPerYear, 0.99d);
ValueAtRisk95 = GetValueAtRisk(listPerformance, tradingDaysPerYear, 0.95d);
var drawdownMetrics = Statistics.CalculateDrawdownMetrics(equity, 3);
Drawdown = drawdownMetrics.Drawdown;
DrawdownRecovery = drawdownMetrics.DrawdownRecovery;
}
/// <summary>
/// Initializes a new instance of the <see cref="PortfolioStatistics"/> class
/// </summary>
public PortfolioStatistics()
{
}
/// <summary>
/// Annualized return statistic calculated as an average of daily trading performance multiplied by the number of trading days per year.
/// </summary>
/// <param name="performance">Dictionary collection of double performance values</param>
/// <param name="tradingDaysPerYear">Trading days per year for the assets in portfolio</param>
/// <remarks>May be inaccurate for forex algorithms with more trading days in a year</remarks>
/// <returns>Double annual performance percentage</returns>
private static decimal GetAnnualPerformance(List<double> performance, int tradingDaysPerYear)
{
try
{
return Statistics.AnnualPerformance(performance, tradingDaysPerYear).SafeDecimalCast();
}
catch (ArgumentException ex)
{
var partialSums = 0.0;
var points = 0;
double troublePoint = default;
foreach (var point in performance)
{
points++;
partialSums += point;
if (Math.Pow(partialSums / points, tradingDaysPerYear).IsNaNOrInfinity())
{
troublePoint = point;
break;
}
}
throw new ArgumentException($"PortfolioStatistics.GetAnnualPerformance(): An exception was thrown when trying to cast the annual performance value due to the following performance point: {troublePoint}. " +
$"The exception thrown was the following: {ex.Message}.");
}
}
private static decimal GetValueAtRisk(
List<double> performance,
int lookbackPeriodDays,
double confidenceLevel,
int rounding = 3)
{
var periodPerformance = performance.TakeLast(lookbackPeriodDays);
var mean = periodPerformance.Mean();
var standardDeviation = periodPerformance.StandardDeviation();
var valueAtRisk = (decimal)Normal.InvCDF(mean, standardDeviation, 1 - confidenceLevel);
return Math.Round(valueAtRisk, rounding);
}
}
}
+320
View File
@@ -0,0 +1,320 @@
/*
* 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 MathNet.Numerics.Distributions;
using MathNet.Numerics.Statistics;
using QuantConnect.Logging;
namespace QuantConnect.Statistics
{
/// <summary>
/// Calculate all the statistics required from the backtest, based on the equity curve and the profit loss statement.
/// </summary>
/// <remarks>This is a particularly ugly class and one of the first ones written. It should be thrown out and re-written.</remarks>
public class Statistics
{
/// <summary>
/// Annual compounded returns statistic based on the final-starting capital and years.
/// </summary>
/// <param name="startingCapital">Algorithm starting capital</param>
/// <param name="finalCapital">Algorithm final capital</param>
/// <param name="years">Years trading</param>
/// <returns>Decimal fraction for annual compounding performance</returns>
public static decimal CompoundingAnnualPerformance(decimal startingCapital, decimal finalCapital, decimal years)
{
if (years == 0 || startingCapital == 0)
{
return 0;
}
var power = 1 / (double)years;
var baseNumber = (double)finalCapital / (double)startingCapital;
var result = Math.Pow(baseNumber, power) - 1;
return result.IsNaNOrInfinity() ? 0 : result.SafeDecimalCast();
}
/// <summary>
/// Annualized return statistic calculated as an average of daily trading performance multiplied by the number of trading days per year.
/// </summary>
/// <param name="performance">Dictionary collection of double performance values</param>
/// <param name="tradingDaysPerYear">Trading days per year for the assets in portfolio</param>
/// <remarks>May be unaccurate for forex algorithms with more trading days in a year</remarks>
/// <returns>Double annual performance percentage</returns>
public static double AnnualPerformance(List<double> performance, double tradingDaysPerYear)
{
return Math.Pow((performance.Average() + 1), tradingDaysPerYear) - 1;
}
/// <summary>
/// Annualized variance statistic calculation using the daily performance variance and trading days per year.
/// </summary>
/// <param name="performance"></param>
/// <param name="tradingDaysPerYear"></param>
/// <remarks>Invokes the variance extension in the MathNet Statistics class</remarks>
/// <returns>Annual variance value</returns>
public static double AnnualVariance(List<double> performance, double tradingDaysPerYear)
{
var variance = performance.Variance();
return variance.IsNaNOrZero() ? 0 : variance * tradingDaysPerYear;
}
/// <summary>
/// Annualized standard deviation
/// </summary>
/// <param name="performance">Collection of double values for daily performance</param>
/// <param name="tradingDaysPerYear">Number of trading days for the assets in portfolio to get annualize standard deviation.</param>
/// <remarks>
/// 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.
/// </remarks>
/// <returns>Value for annual standard deviation</returns>
public static double AnnualStandardDeviation(List<double> performance, double tradingDaysPerYear)
{
return Math.Sqrt(AnnualVariance(performance, tradingDaysPerYear));
}
/// <summary>
/// Annualized variance statistic calculation using the daily performance variance and trading days per year.
/// </summary>
/// <param name="performance"></param>
/// <param name="tradingDaysPerYear"></param>
/// <param name="minimumAcceptableReturn">Minimum acceptable return</param>
/// <remarks>Invokes the variance extension in the MathNet Statistics class</remarks>
/// <returns>Annual variance value</returns>
public static double AnnualDownsideVariance(List<double> performance, double tradingDaysPerYear, double minimumAcceptableReturn = 0)
{
return AnnualVariance(performance.Where(ret => ret < minimumAcceptableReturn).ToList(), tradingDaysPerYear);
}
/// <summary>
/// Annualized downside standard deviation
/// </summary>
/// <param name="performance">Collection of double values for daily performance</param>
/// <param name="tradingDaysPerYear">Number of trading days for the assets in portfolio to get annualize standard deviation.</param>
/// <param name="minimumAcceptableReturn">Minimum acceptable return</param>
/// <returns>Value for annual downside standard deviation</returns>
public static double AnnualDownsideStandardDeviation(List<double> performance, double tradingDaysPerYear, double minimumAcceptableReturn = 0)
{
return Math.Sqrt(AnnualDownsideVariance(performance, tradingDaysPerYear, minimumAcceptableReturn));
}
/// <summary>
/// Tracking error volatility (TEV) statistic - a measure of how closely a portfolio follows the index to which it is benchmarked
/// </summary>
/// <remarks>If algo = benchmark, TEV = 0</remarks>
/// <param name="algoPerformance">Double collection of algorithm daily performance values</param>
/// <param name="benchmarkPerformance">Double collection of benchmark daily performance values</param>
/// <param name="tradingDaysPerYear">Number of trading days per year</param>
/// <returns>Value for tracking error</returns>
public static double TrackingError(List<double> algoPerformance, List<double> benchmarkPerformance, double tradingDaysPerYear)
{
// Un-equal lengths will blow up other statistics, but this will handle the case here
if (algoPerformance.Count != benchmarkPerformance.Count)
{
return 0.0;
}
var performanceDifference = new List<double>();
for (var i = 0; i < algoPerformance.Count; i++)
{
performanceDifference.Add(algoPerformance[i] - benchmarkPerformance[i]);
}
return Math.Sqrt(AnnualVariance(performanceDifference, tradingDaysPerYear));
}
/// <summary>
/// Sharpe ratio with respect to risk free rate: measures excess of return per unit of risk.
/// </summary>
/// <remarks>With risk defined as the algorithm's volatility</remarks>
/// <param name="averagePerformance">Average daily performance</param>
/// <param name="standardDeviation">Standard deviation of the daily performance</param>
/// <param name="riskFreeRate">The risk free rate</param>
/// <returns>Value for sharpe ratio</returns>
public static double SharpeRatio(double averagePerformance, double standardDeviation, double riskFreeRate)
{
return standardDeviation == 0 ? 0 : (averagePerformance - riskFreeRate) / standardDeviation;
}
/// <summary>
/// Sharpe ratio with respect to risk free rate: measures excess of return per unit of risk.
/// </summary>
/// <remarks>With risk defined as the algorithm's volatility</remarks>
/// <param name="averagePerformance">Average daily performance</param>
/// <param name="standardDeviation">Standard deviation of the daily performance</param>
/// <param name="riskFreeRate">The risk free rate</param>
/// <returns>Value for sharpe ratio</returns>
public static decimal SharpeRatio(decimal averagePerformance, decimal standardDeviation, decimal riskFreeRate)
{
return SharpeRatio((double)averagePerformance, (double)standardDeviation, (double)riskFreeRate).SafeDecimalCast();
}
/// <summary>
/// Sharpe ratio with respect to risk free rate: measures excess of return per unit of risk.
/// </summary>
/// <remarks>With risk defined as the algorithm's volatility</remarks>
/// <param name="algoPerformance">Collection of double values for the algorithm daily performance</param>
/// <param name="riskFreeRate">The risk free rate</param>
/// <param name="tradingDaysPerYear">Trading days per year for the assets in portfolio</param>
/// <returns>Value for sharpe ratio</returns>
public static double SharpeRatio(List<double> algoPerformance, double riskFreeRate, double tradingDaysPerYear)
{
return SharpeRatio(AnnualPerformance(algoPerformance, tradingDaysPerYear), AnnualStandardDeviation(algoPerformance, tradingDaysPerYear), riskFreeRate);
}
/// <summary>
/// Sortino ratio with respect to risk free rate: measures excess of return per unit of downside risk.
/// </summary>
/// <remarks>With risk defined as the algorithm's volatility</remarks>
/// <param name="algoPerformance">Collection of double values for the algorithm daily performance</param>
/// <param name="riskFreeRate">The risk free rate</param>
/// <param name="tradingDaysPerYear">Trading days per year for the assets in portfolio</param>
/// <param name="minimumAcceptableReturn">Minimum acceptable return for Sortino ratio calculation</param>
/// <returns>Value for Sortino ratio</returns>
public static double SortinoRatio(List<double> algoPerformance, double riskFreeRate, double tradingDaysPerYear, double minimumAcceptableReturn = 0)
{
return SharpeRatio(AnnualPerformance(algoPerformance, tradingDaysPerYear), AnnualDownsideStandardDeviation(algoPerformance, tradingDaysPerYear, minimumAcceptableReturn), riskFreeRate);
}
/// <summary>
/// Helper method to calculate the probabilistic sharpe ratio
/// </summary>
/// <param name="listPerformance">The list of algorithm performance values</param>
/// <param name="benchmarkSharpeRatio">The benchmark sharpe ratio to use</param>
/// <param name="riskFreeRate">The risk free rate for each performance sample</param>
/// <returns>Probabilistic Sharpe Ratio</returns>
public static double ProbabilisticSharpeRatio(List<double> listPerformance,
double benchmarkSharpeRatio,
double riskFreeRate = 0)
{
var observedSharpeRatio = ObservedSharpeRatio(listPerformance, riskFreeRate);
var skewness = listPerformance.Skewness();
var kurtosis = listPerformance.Kurtosis();
var operandA = skewness * observedSharpeRatio;
var operandB = ((kurtosis - 1) / 4) * (Math.Pow(observedSharpeRatio, 2));
// Calculated standard deviation of point estimate
var estimateStandardDeviation = Math.Pow((1 - operandA + operandB) / (listPerformance.Count - 1), 0.5);
if (double.IsNaN(estimateStandardDeviation))
{
return 0;
}
// Calculate PSR(benchmark)
var value = estimateStandardDeviation.IsNaNOrZero() ? 0 : (observedSharpeRatio - benchmarkSharpeRatio) / estimateStandardDeviation;
return (new Normal()).CumulativeDistribution(value);
}
/// <summary>
/// Calculates the observed sharpe ratio
/// </summary>
/// <param name="listPerformance">The performance samples to use</param>
/// <param name="riskFreeRate">The risk free rate for each performance sample</param>
/// <returns>The observed sharpe ratio</returns>
public static double ObservedSharpeRatio(List<double> listPerformance, double riskFreeRate = 0)
{
var performanceAverage = listPerformance.Average() - riskFreeRate;
var standardDeviation = listPerformance.StandardDeviation();
// we don't annualize it
return standardDeviation.IsNaNOrZero() ? 0 : performanceAverage / standardDeviation;
}
/// <summary>
/// Calculate the drawdown between a high and current value
/// </summary>
/// <param name="current">Current value</param>
/// <param name="high">Latest maximum</param>
/// <param name="roundingDecimals">Digits to round the result too</param>
/// <returns>Drawdown percentage</returns>
public static decimal DrawdownPercent(decimal current, decimal high, int roundingDecimals = 2)
{
if (high == 0)
{
throw new ArgumentException("High value must not be 0");
}
var drawdownPercentage = ((current / high) - 1) * 100;
return Math.Round(drawdownPercentage, roundingDecimals);
}
/// <summary>
/// Calculates the maximum drawdown percentage and the maximum recovery time (in days)
/// from a historical equity time series.
/// </summary>
/// <param name="equityOverTime">Time series of equity values indexed by date</param>
/// <param name="rounding">Number of decimals to round the results to</param>
/// <returns>A <see cref="DrawdownMetrics"/> object containing MaxDrawdown (percentage) and MaxRecoveryTime (in days)</returns>
public static DrawdownMetrics CalculateDrawdownMetrics(SortedDictionary<DateTime, decimal> equityOverTime, int rounding = 2)
{
decimal maxDrawdown = 0m;
decimal maxRecoveryTime = 0m;
try
{
if (equityOverTime.Count < 2) return new DrawdownMetrics(0m, 0);
var equityList = equityOverTime.ToList();
var peakEquity = equityList[0].Value;
var peakDate = equityList[0].Key;
DateTime? drawdownStartDate = null;
foreach (var point in equityList)
{
// Update peak equity if a new high is reached (or matched)
if (point.Value >= peakEquity)
{
// If we were in a drawdown, calculate recovery time
if (drawdownStartDate.HasValue)
{
var recoveryDays = (decimal)(point.Key - drawdownStartDate.Value).TotalDays;
maxRecoveryTime = Math.Max(maxRecoveryTime, recoveryDays);
drawdownStartDate = null;
}
peakEquity = point.Value;
peakDate = point.Key;
}
// Calculate current drawdown from peak
var currentDrawdown = (point.Value / peakEquity) - 1;
if (currentDrawdown < 0)
{
maxDrawdown = Math.Min(maxDrawdown, currentDrawdown);
// Mark the start of the drawdown period
if (!drawdownStartDate.HasValue)
{
drawdownStartDate = peakDate;
}
}
}
// Return absolute drawdown percentage and max recovery time in days
return new DrawdownMetrics(Math.Round(Math.Abs(maxDrawdown), rounding), (int)maxRecoveryTime);
}
catch (Exception err)
{
Log.Error(err);
return new DrawdownMetrics(0m, 0);
}
}
} // End of Statistics
} // End of Namespace
+390
View File
@@ -0,0 +1,390 @@
/*
* 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
{
/// <summary>
/// The <see cref="StatisticsBuilder"/> class creates summary and rolling statistics from trades, equity and benchmark points
/// </summary>
public static class StatisticsBuilder
{
/// <summary>
/// Generates the statistics and returns the results
/// </summary>
/// <param name="trades">The list of closed trades</param>
/// <param name="profitLoss">Trade record of profits and losses</param>
/// <param name="pointsEquity">The list of daily equity values</param>
/// <param name="pointsPerformance">The list of algorithm performance values</param>
/// <param name="pointsBenchmark">The list of benchmark values</param>
/// <param name="pointsPortfolioTurnover">The list of portfolio turnover daily samples</param>
/// <param name="startingCapital">The algorithm starting capital</param>
/// <param name="totalFees">The total fees</param>
/// <param name="totalOrders">The total number of transactions</param>
/// <param name="estimatedStrategyCapacity">The estimated capacity of this strategy</param>
/// <param name="accountCurrencySymbol">The account currency symbol</param>
/// <param name="transactions">
/// The transaction manager to get number of winning and losing transactions
/// </param>
/// <param name="riskFreeInterestRateModel">The risk free interest rate model to use</param>
/// <param name="tradingDaysPerYear">The number of trading days per year</param>
/// <returns>Returns a <see cref="StatisticsResults"/> object</returns>
public static StatisticsResults Generate(
List<Trade> trades,
SortedDictionary<DateTime, decimal> profitLoss,
List<ISeriesPoint> pointsEquity,
List<ISeriesPoint> pointsPerformance,
List<ISeriesPoint> pointsBenchmark,
List<ISeriesPoint> 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);
}
/// <summary>
/// Returns the performance of the algorithm in the specified date range
/// </summary>
/// <param name="fromDate">The initial date of the range</param>
/// <param name="toDate">The final date of the range</param>
/// <param name="trades">The list of closed trades</param>
/// <param name="profitLoss">Trade record of profits and losses</param>
/// <param name="equity">The list of daily equity values</param>
/// <param name="pointsPerformance">The list of algorithm performance values</param>
/// <param name="pointsBenchmark">The list of benchmark values</param>
/// <param name="pointsPortfolioTurnover">The list of portfolio turnover daily samples</param>
/// <param name="startingCapital">The algorithm starting capital</param>
/// <param name="transactions">
/// The transaction manager to get number of winning and losing transactions
/// </param>
/// <param name="riskFreeInterestRateModel">The risk free interest rate model to use</param>
/// <param name="tradingDaysPerYear">The number of trading days per year</param>
/// <returns>The algorithm performance</returns>
private static AlgorithmPerformance GetAlgorithmPerformance(
DateTime fromDate,
DateTime toDate,
List<Trade> trades,
SortedDictionary<DateTime, decimal> profitLoss,
SortedDictionary<DateTime, decimal> equity,
List<ISeriesPoint> pointsPerformance,
List<ISeriesPoint> pointsBenchmark,
List<ISeriesPoint> pointsPortfolioTurnover,
decimal startingCapital,
SecurityTransactionManager transactions,
IRiskFreeInterestRateModel riskFreeInterestRateModel,
int tradingDaysPerYear)
{
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));
// 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<DateTime, decimal>(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);
}
/// <summary>
/// Returns the rolling performances of the algorithm
/// </summary>
/// <param name="firstDate">The first date of the total period</param>
/// <param name="lastDate">The last date of the total period</param>
/// <param name="trades">The list of closed trades</param>
/// <param name="profitLoss">Trade record of profits and losses</param>
/// <param name="equity">The list of daily equity values</param>
/// <param name="pointsPerformance">The list of algorithm performance values</param>
/// <param name="pointsBenchmark">The list of benchmark values</param>
/// <param name="pointsPortfolioTurnover">The list of portfolio turnover daily samples</param>
/// <param name="startingCapital">The algorithm starting capital</param>
/// <param name="transactions">
/// The transaction manager to get number of winning and losing transactions
/// </param>
/// <param name="riskFreeInterestRateModel">The risk free interest rate model to use</param>
/// <param name="tradingDaysPerYear">The number of trading days per year</param>
/// <returns>A dictionary with the rolling performances</returns>
private static Dictionary<string, AlgorithmPerformance> GetRollingPerformances(
DateTime firstDate,
DateTime lastDate,
List<Trade> trades,
SortedDictionary<DateTime, decimal> profitLoss,
SortedDictionary<DateTime, decimal> equity,
List<ISeriesPoint> pointsPerformance,
List<ISeriesPoint> pointsBenchmark,
List<ISeriesPoint> pointsPortfolioTurnover,
decimal startingCapital,
SecurityTransactionManager transactions,
IRiskFreeInterestRateModel riskFreeInterestRateModel,
int tradingDaysPerYear)
{
var rollingPerformances = new Dictionary<string, AlgorithmPerformance>();
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;
}
/// <summary>
/// Returns a summary of the algorithm performance as a dictionary
/// </summary>
private static Dictionary<string, string> 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<string, string>
{
{ 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() },
};
}
/// <summary>
/// Helper class for rolling statistics
/// </summary>
private class PeriodRange
{
internal DateTime StartDate { get; set; }
internal DateTime EndDate { get; set; }
}
/// <summary>
/// Gets a list of date ranges for the requested monthly period
/// </summary>
/// <remarks>The first and last ranges created are partial periods</remarks>
/// <param name="periodMonths">The number of months in the period (valid inputs are [1, 3, 6, 12])</param>
/// <param name="firstDate">The first date of the total period</param>
/// <param name="lastDate">The last date of the total period</param>
/// <returns>The list of date ranges</returns>
private static IEnumerable<PeriodRange> GetPeriodRanges(int periodMonths, DateTime firstDate, DateTime lastDate)
{
// get end dates
var date = lastDate.Date;
var endDates = new List<DateTime>();
do
{
endDates.Add(date);
date = new DateTime(date.Year, date.Month, 1).AddDays(-1);
} while (date >= firstDate);
// build period ranges
var ranges = new List<PeriodRange> { 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;
}
/// <summary>
/// Convert the charting data into an equity array.
/// </summary>
/// <remarks>This is required to convert the equity plot into a usable form for the statistics calculation</remarks>
/// <param name="points">ChartPoints Array</param>
/// <param name="fromDate">An optional starting date</param>
/// <param name="toDate">An optional ending date</param>
/// <returns>SortedDictionary of the equity decimal values ordered in time</returns>
private static SortedDictionary<DateTime, decimal> ChartPointToDictionary(IEnumerable<ISeriesPoint> points, DateTime? fromDate = null, DateTime? toDate = null)
{
var dictionary = new SortedDictionary<DateTime, decimal>();
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;
}
/// <summary>
/// Gets the value of a point, either ChartPoint.y or Candlestick.Close
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static decimal GetPointValue(ISeriesPoint point)
{
if (point is ChartPoint)
{
return ((ChartPoint)point).y.Value;
}
return ((Candlestick)point).Close.Value;
}
/// <summary>
/// Yields pairs of date and percentage change for the period
/// </summary>
/// <param name="points">The values to calculate percentage change for</param>
/// <param name="fromDate">Starting date (inclusive)</param>
/// <param name="toDate">Ending date (inclusive)</param>
/// <returns>Pairs of date and percentage change</returns>
public static IEnumerable<KeyValuePair<DateTime, double>> CreateBenchmarkDifferences(IEnumerable<KeyValuePair<DateTime, decimal>> 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<DateTime, double>(dt, deltaPercentage);
}
else
{
firstValueSkipped = true;
}
}
dtPrevious = dt;
previous = value;
}
}
/// <summary>
/// Skips the first two entries from the given points and divides each entry by 100
/// </summary>
/// <param name="points">The values to divide by 100</param>
/// <returns>Pairs of date and performance value divided by 100</returns>
public static IEnumerable<KeyValuePair<DateTime, double>> PreprocessPerformanceValues(IEnumerable<KeyValuePair<DateTime, decimal>> 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<DateTime, double>(kvp.Key, (double)(kvp.Value / 100));
}
}
}
}
+74
View File
@@ -0,0 +1,74 @@
/*
* 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.Collections.Generic;
namespace QuantConnect.Statistics
{
/// <summary>
/// The <see cref="StatisticsResults"/> class represents total and rolling statistics for an algorithm
/// </summary>
public class StatisticsResults
{
/// <summary>
/// The performance of the algorithm over the whole period
/// </summary>
public AlgorithmPerformance TotalPerformance { get; private set; }
/// <summary>
/// The rolling performance of the algorithm over 1, 3, 6, 12 month periods
/// </summary>
public Dictionary<string, AlgorithmPerformance> RollingPerformances { get; private set; }
/// <summary>
/// Returns a summary of the algorithm performance as a dictionary
/// </summary>
public Dictionary<string, string> Summary { get; private set; }
/// <summary>
/// Initializes a new instance of the <see cref="StatisticsResults"/> class
/// </summary>
/// <param name="totalPerformance">The algorithm total performance</param>
/// <param name="rollingPerformances">The algorithm rolling performances</param>
/// <param name="summary">The summary performance dictionary</param>
public StatisticsResults(AlgorithmPerformance totalPerformance, Dictionary<string, AlgorithmPerformance> rollingPerformances, Dictionary<string, string> summary)
{
TotalPerformance = totalPerformance;
RollingPerformances = rollingPerformances;
Summary = summary;
}
/// <summary>
/// Initializes a new instance of the <see cref="StatisticsResults"/> class
/// </summary>
public StatisticsResults()
{
TotalPerformance = new AlgorithmPerformance();
RollingPerformances = new Dictionary<string, AlgorithmPerformance>();
Summary = new Dictionary<string, string>();
}
internal void AddCustomSummaryStatistics(IDictionary<string, string> customSummary)
{
foreach (var kvp in customSummary)
{
if (!Summary.ContainsKey(kvp.Key))
{
Summary[kvp.Key] = kvp.Value;
}
}
}
}
}
+177
View File
@@ -0,0 +1,177 @@
/*
* 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 Newtonsoft.Json;
using System;
using System.Collections.Generic;
namespace QuantConnect.Statistics
{
/// <summary>
/// Represents a closed trade
/// </summary>
public class Trade
{
private List<Symbol> _symbols;
/// <summary>
/// A unique identifier for the trade
/// </summary>
public string Id { get; set; }
/// <summary>
/// The symbol of the traded instrument
/// </summary>
[Obsolete("Use Symbols property instead")]
[JsonIgnore]
public Symbol Symbol
{
get
{
return _symbols != null && _symbols.Count > 0 ? _symbols[0] : Symbol.Empty;
}
private set
{
_symbols = new List<Symbol>() { value };
}
}
/// <summary>
/// Just needed so that "Symbol" is never serialized but can be deserialized, if present, for backward compatibility
/// </summary>
[JsonProperty("Symbol")]
private Symbol SymbolForDeserialization { set => Symbol = value; }
/// <summary>
/// The symbol associated to the traded instruments
/// </summary>
public List<Symbol> Symbols
{
get { return _symbols; }
set { _symbols = value; }
}
/// <summary>
/// The date and time the trade was opened
/// </summary>
public DateTime EntryTime { get; set; }
/// <summary>
/// The price at which the trade was opened (or the average price if multiple entries)
/// </summary>
public decimal EntryPrice { get; set; }
/// <summary>
/// The direction of the trade (Long or Short)
/// </summary>
public TradeDirection Direction { get; set; }
/// <summary>
/// The total unsigned quantity of the trade
/// </summary>
public decimal Quantity { get; set; }
/// <summary>
/// The date and time the trade was closed
/// </summary>
public DateTime ExitTime { get; set; }
/// <summary>
/// The price at which the trade was closed (or the average price if multiple exits)
/// </summary>
public decimal ExitPrice { get; set; }
/// <summary>
/// The gross profit/loss of the trade (as account currency)
/// </summary>
public decimal ProfitLoss { get; set; }
/// <summary>
/// The total fees associated with the trade (always positive value) (as account currency)
/// </summary>
public decimal TotalFees { get; set; }
/// <summary>
/// The Maximum Adverse Excursion (as account currency)
/// </summary>
public decimal MAE { get; set; }
/// <summary>
/// The Maximum Favorable Excursion (as account currency)
/// </summary>
public decimal MFE { get; set; }
/// <summary>
/// Returns the duration of the trade
/// </summary>
public TimeSpan Duration
{
get { return ExitTime - EntryTime; }
}
/// <summary>
/// Returns the amount of profit given back before the trade was closed
/// </summary>
public decimal EndTradeDrawdown { get; set; }
/// <summary>
/// Returns whether the trade was profitable (is a win) or not (a loss)
/// </summary>
/// <returns>True if the trade was profitable</returns>
/// <remarks>
/// Even when a trade is not profitable, it may still be a win:
/// - For an ITM option buyer, an option assignment trade is not profitable (money was paid),
/// but it might count as a win if the ITM amount is greater than the amount paid for the option.
/// - For an ITM option seller, an option assignment trade is profitable (money was received),
/// but it might count as a loss if the ITM amount is less than the amount received for the option.
/// </remarks>
public bool IsWin { get; set; }
/// <summary>
/// The IDs of the orders related to this trade
/// </summary>
public HashSet<int> OrderIds { get; init; } = new HashSet<int>();
/// <summary>
/// Creates a new instance of the <see cref="Trade"/> class
/// </summary>
public Trade()
{
}
/// <summary>
/// Creates a new instance of the <see cref="Trade"/> class by copying another trade
/// </summary>
/// <param name="other">The trade to copy</param>
public Trade(Trade other)
{
Id = other.Id;
_symbols = other._symbols != null ? [.. other._symbols] : null;
EntryTime = other.EntryTime;
EntryPrice = other.EntryPrice;
Direction = other.Direction;
Quantity = other.Quantity;
ExitTime = other.ExitTime;
ExitPrice = other.ExitPrice;
ProfitLoss = other.ProfitLoss;
TotalFees = other.TotalFees;
MAE = other.MAE;
MFE = other.MFE;
EndTradeDrawdown = other.EndTradeDrawdown;
IsWin = other.IsWin;
OrderIds = [.. other.OrderIds];
}
}
}
+639
View File
@@ -0,0 +1,639 @@
/*
* 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 QuantConnect.Data.Market;
using QuantConnect.Interfaces;
using QuantConnect.Orders;
using QuantConnect.Securities;
using QuantConnect.Util;
using System;
using System.Collections.Generic;
using System.Linq;
namespace QuantConnect.Statistics
{
/// <summary>
/// The <see cref="TradeBuilder"/> class generates trades from executions and market price updates
/// </summary>
public class TradeBuilder : ITradeBuilder
{
private class TradeState
{
internal Trade Trade { get; set; }
internal decimal MaxProfit { get; set; }
internal decimal MaxDrawdown { get; set; }
/// <summary>
/// Updates the drawdown state given the current profit
/// </summary>
public void UpdateDrawdown(decimal currentProfit)
{
if (currentProfit < MaxProfit)
{
// There is a drawdown, but we only care about the maximum drawdown
var drawdown = MaxProfit - currentProfit;
if (drawdown > MaxDrawdown)
{
MaxDrawdown = drawdown;
}
}
else
{
// New maximum profit
MaxProfit = currentProfit;
}
}
}
/// <summary>
/// Helper class to manage pending trades and market price updates for a symbol
/// </summary>
private class Position
{
internal List<TradeState> PendingTrades { get; set; }
internal List<OrderEvent> PendingFills { get; set; }
internal decimal TotalFees { get; set; }
internal decimal MaxPrice { get; set; }
internal decimal MinPrice { get; set; }
public Position()
{
PendingTrades = new List<TradeState>();
PendingFills = new List<OrderEvent>();
}
}
private const int LiveModeMaxTradeCount = 10000;
private const int LiveModeMaxTradeAgeMonths = 12;
private const int MaxOrderIdCacheSize = 1000;
private readonly List<Trade> _closedTrades = new List<Trade>();
private readonly Dictionary<Symbol, Position> _positions = new Dictionary<Symbol, Position>();
private readonly FixedSizeHashQueue<int> _ordersWithFeesAssigned = new FixedSizeHashQueue<int>(MaxOrderIdCacheSize);
private readonly FillGroupingMethod _groupingMethod;
private readonly FillMatchingMethod _matchingMethod;
private SecurityManager _securities;
private bool _liveMode;
/// <summary>
/// Initializes a new instance of the <see cref="TradeBuilder"/> class
/// </summary>
public TradeBuilder(FillGroupingMethod groupingMethod, FillMatchingMethod matchingMethod)
{
_groupingMethod = groupingMethod;
_matchingMethod = matchingMethod;
}
/// <summary>
/// Sets the live mode flag
/// </summary>
/// <param name="live">The live mode flag</param>
public void SetLiveMode(bool live)
{
_liveMode = live;
}
/// <summary>
/// Sets the security manager instance
/// </summary>
/// <param name="securities">The security manager</param>
public void SetSecurityManager(SecurityManager securities)
{
_securities = securities;
}
/// <summary>
/// The list of closed trades
/// </summary>
public List<Trade> ClosedTrades
{
get
{
lock (_closedTrades)
{
return new List<Trade>(_closedTrades);
}
}
}
/// <summary>
/// Returns true if there is an open position for the symbol
/// </summary>
/// <param name="symbol">The symbol</param>
/// <returns>true if there is an open position for the symbol</returns>
public bool HasOpenPosition(Symbol symbol)
{
Position position;
if (!_positions.TryGetValue(symbol, out position)) return false;
if (_groupingMethod == FillGroupingMethod.FillToFill)
return position.PendingTrades.Count > 0;
return position.PendingFills.Count > 0;
}
/// <summary>
/// Sets the current market price for the symbol
/// </summary>
/// <param name="symbol"></param>
/// <param name="price"></param>
public void SetMarketPrice(Symbol symbol, decimal price)
{
Position position;
if (!_positions.TryGetValue(symbol, out position)) return;
if (price > position.MaxPrice)
position.MaxPrice = price;
else if (price < position.MinPrice)
position.MinPrice = price;
for (var i = 0; i < position.PendingTrades.Count; i++)
{
var tradeState = position.PendingTrades[i];
var trade = tradeState.Trade;
var currentProfit = trade.Direction == TradeDirection.Long ? price - trade.EntryPrice : trade.EntryPrice - price;
tradeState.UpdateDrawdown(currentProfit);
}
}
/// <summary>
/// Applies a split to the trade builder
/// </summary>
/// <param name="split">The split to be applied</param>
/// <param name="liveMode">True if live mode, false for backtest</param>
/// <param name="dataNormalizationMode">The <see cref="DataNormalizationMode"/> for this security</param>
public void ApplySplit(Split split, bool liveMode, DataNormalizationMode dataNormalizationMode)
{
// only apply splits to equities, in live or raw data mode, and for open positions
if (split.Symbol.SecurityType != SecurityType.Equity ||
(!liveMode && dataNormalizationMode != DataNormalizationMode.Raw) ||
!_positions.TryGetValue(split.Symbol, out var position))
{
return;
}
position.MinPrice *= split.SplitFactor;
position.MaxPrice *= split.SplitFactor;
foreach (var tradeState in position.PendingTrades)
{
tradeState.Trade.Quantity /= split.SplitFactor;
tradeState.Trade.EntryPrice *= split.SplitFactor;
tradeState.Trade.ExitPrice *= split.SplitFactor;
tradeState.MaxProfit *= split.SplitFactor;
tradeState.MaxDrawdown *= split.SplitFactor;
}
foreach (var pendingFill in position.PendingFills)
{
pendingFill.FillQuantity /= split.SplitFactor;
pendingFill.FillPrice *= split.SplitFactor;
if (pendingFill.LimitPrice.HasValue)
{
pendingFill.LimitPrice *= split.SplitFactor;
}
if (pendingFill.StopPrice.HasValue)
{
pendingFill.StopPrice *= split.SplitFactor;
}
if (pendingFill.TriggerPrice.HasValue)
{
pendingFill.TriggerPrice *= split.SplitFactor;
}
}
}
/// <summary>
/// Processes a new fill, eventually creating new trades
/// </summary>
/// <param name="fill">The new fill order event</param>
/// <param name="securityConversionRate">The current security market conversion rate into the account currency</param>
/// <param name="feeInAccountCurrency">The current order fee in the account currency</param>
/// <param name="multiplier">The contract multiplier</param>
public void ProcessFill(OrderEvent fill,
decimal securityConversionRate,
decimal feeInAccountCurrency,
decimal multiplier = 1.0m)
{
// If we have multiple fills per order, we assign the order fee only to its first fill
// to avoid counting the same order fee multiple times.
var orderFee = 0m;
if (!_ordersWithFeesAssigned.Contains(fill.OrderId))
{
orderFee = feeInAccountCurrency;
_ordersWithFeesAssigned.Add(fill.OrderId);
}
switch (_groupingMethod)
{
case FillGroupingMethod.FillToFill:
ProcessFillUsingFillToFill(fill.Clone(), orderFee, securityConversionRate, multiplier);
break;
case FillGroupingMethod.FlatToFlat:
ProcessFillUsingFlatToFlat(fill.Clone(), orderFee, securityConversionRate, multiplier);
break;
case FillGroupingMethod.FlatToReduced:
ProcessFillUsingFlatToReduced(fill.Clone(), orderFee, securityConversionRate, multiplier);
break;
}
}
private void ProcessFillUsingFillToFill(OrderEvent fill, decimal orderFee, decimal conversionRate, decimal multiplier)
{
Position position;
if (!_positions.TryGetValue(fill.Symbol, out position) || position.PendingTrades.Count == 0)
{
// no pending trades for symbol
_positions[fill.Symbol] = new Position
{
PendingTrades = new List<TradeState>
{
new TradeState
{
Trade = new Trade
{
Symbols = [fill.Symbol],
EntryTime = fill.UtcTime,
EntryPrice = fill.FillPrice,
Direction = fill.FillQuantity > 0 ? TradeDirection.Long : TradeDirection.Short,
Quantity = fill.AbsoluteFillQuantity,
TotalFees = orderFee,
OrderIds = new HashSet<int>() { fill.OrderId }
}
}
},
MinPrice = fill.FillPrice,
MaxPrice = fill.FillPrice
};
return;
}
SetMarketPrice(fill.Symbol, fill.FillPrice);
var index = _matchingMethod == FillMatchingMethod.FIFO ? 0 : position.PendingTrades.Count - 1;
if (Math.Sign(fill.FillQuantity) == (position.PendingTrades[index].Trade.Direction == TradeDirection.Long ? +1 : -1))
{
// execution has same direction of trade
position.PendingTrades.Add(new TradeState
{
Trade = new Trade
{
Symbols = [fill.Symbol],
EntryTime = fill.UtcTime,
EntryPrice = fill.FillPrice,
Direction = fill.FillQuantity > 0 ? TradeDirection.Long : TradeDirection.Short,
Quantity = fill.AbsoluteFillQuantity,
TotalFees = orderFee,
OrderIds = new HashSet<int>() { fill.OrderId }
}
});
}
else
{
// execution has opposite direction of trade
var totalExecutedQuantity = 0m;
var orderFeeAssigned = false;
while (position.PendingTrades.Count > 0 && Math.Abs(totalExecutedQuantity) < fill.AbsoluteFillQuantity)
{
var tradeState = position.PendingTrades[index];
var trade = tradeState.Trade;
var absoluteUnexecutedQuantity = fill.AbsoluteFillQuantity - Math.Abs(totalExecutedQuantity);
if (absoluteUnexecutedQuantity >= trade.Quantity)
{
totalExecutedQuantity -= trade.Quantity * (trade.Direction == TradeDirection.Long ? +1 : -1);
position.PendingTrades.RemoveAt(index);
trade.OrderIds.Add(fill.OrderId);
if (index > 0 && _matchingMethod == FillMatchingMethod.LIFO) index--;
trade.ExitTime = fill.UtcTime;
trade.ExitPrice = fill.FillPrice;
trade.ProfitLoss = Math.Round((trade.ExitPrice - trade.EntryPrice) * trade.Quantity * (trade.Direction == TradeDirection.Long ? +1 : -1) * conversionRate * multiplier, 2);
// if closing multiple trades with the same order, assign order fee only once
trade.TotalFees += orderFeeAssigned ? 0 : orderFee;
trade.MAE = Math.Round((trade.Direction == TradeDirection.Long ? position.MinPrice - trade.EntryPrice : trade.EntryPrice - position.MaxPrice) * trade.Quantity * conversionRate * multiplier, 2);
trade.MFE = Math.Round((trade.Direction == TradeDirection.Long ? position.MaxPrice - trade.EntryPrice : trade.EntryPrice - position.MinPrice) * trade.Quantity * conversionRate * multiplier, 2);
trade.EndTradeDrawdown = Math.Round(tradeState.MaxDrawdown * trade.Quantity * conversionRate * multiplier, 2);
AddNewTrade(trade, fill);
}
else
{
totalExecutedQuantity += absoluteUnexecutedQuantity * (trade.Direction == TradeDirection.Long ? -1 : +1);
trade.Quantity -= absoluteUnexecutedQuantity;
var newTrade = new Trade
{
Symbols = trade.Symbols,
EntryTime = trade.EntryTime,
EntryPrice = trade.EntryPrice,
Direction = trade.Direction,
Quantity = absoluteUnexecutedQuantity,
ExitTime = fill.UtcTime,
ExitPrice = fill.FillPrice,
ProfitLoss = Math.Round((fill.FillPrice - trade.EntryPrice) * absoluteUnexecutedQuantity * (trade.Direction == TradeDirection.Long ? +1 : -1) * conversionRate * multiplier, 2),
TotalFees = trade.TotalFees + (orderFeeAssigned ? 0 : orderFee),
MAE = Math.Round((trade.Direction == TradeDirection.Long ? position.MinPrice - trade.EntryPrice : trade.EntryPrice - position.MaxPrice) * absoluteUnexecutedQuantity * conversionRate * multiplier, 2),
MFE = Math.Round((trade.Direction == TradeDirection.Long ? position.MaxPrice - trade.EntryPrice : trade.EntryPrice - position.MinPrice) * absoluteUnexecutedQuantity * conversionRate * multiplier, 2),
EndTradeDrawdown = Math.Round(tradeState.MaxDrawdown * absoluteUnexecutedQuantity * conversionRate * multiplier, 2),
OrderIds = new HashSet<int>([..trade.OrderIds, fill.OrderId])
};
AddNewTrade(newTrade, fill);
trade.TotalFees = 0;
}
orderFeeAssigned = true;
}
if (Math.Abs(totalExecutedQuantity) == fill.AbsoluteFillQuantity && position.PendingTrades.Count == 0)
{
_positions.Remove(fill.Symbol);
}
else if (Math.Abs(totalExecutedQuantity) < fill.AbsoluteFillQuantity)
{
// direction reversal
fill.FillQuantity -= totalExecutedQuantity;
position.PendingTrades = new List<TradeState>
{
new TradeState
{
Trade = new Trade
{
Symbols =[fill.Symbol],
EntryTime = fill.UtcTime,
EntryPrice = fill.FillPrice,
Direction = fill.FillQuantity > 0 ? TradeDirection.Long : TradeDirection.Short,
Quantity = fill.AbsoluteFillQuantity,
TotalFees = 0,
OrderIds = new HashSet<int>() { fill.OrderId }
}
}
};
position.MinPrice = fill.FillPrice;
position.MaxPrice = fill.FillPrice;
}
}
}
private void ProcessFillUsingFlatToFlat(OrderEvent fill, decimal orderFee, decimal conversionRate, decimal multiplier)
{
Position position;
if (!_positions.TryGetValue(fill.Symbol, out position) || position.PendingFills.Count == 0)
{
// no pending executions for symbol
_positions[fill.Symbol] = new Position
{
PendingFills = new List<OrderEvent> { fill },
TotalFees = orderFee,
MinPrice = fill.FillPrice,
MaxPrice = fill.FillPrice
};
return;
}
SetMarketPrice(fill.Symbol, fill.FillPrice);
if (Math.Sign(position.PendingFills[0].FillQuantity) == Math.Sign(fill.FillQuantity))
{
// execution has same direction of trade
position.PendingFills.Add(fill);
position.TotalFees += orderFee;
}
else
{
// execution has opposite direction of trade
if (position.PendingFills.Aggregate(0m, (d, x) => d + x.FillQuantity) + fill.FillQuantity == 0 || fill.AbsoluteFillQuantity > Math.Abs(position.PendingFills.Aggregate(0m, (d, x) => d + x.FillQuantity)))
{
// trade closed
position.PendingFills.Add(fill);
position.TotalFees += orderFee;
var reverseQuantity = position.PendingFills.Sum(x => x.FillQuantity);
var index = _matchingMethod == FillMatchingMethod.FIFO ? 0 : position.PendingFills.Count - 1;
var entryTime = position.PendingFills[0].UtcTime;
var totalEntryQuantity = 0m;
var totalExitQuantity = 0m;
var entryAveragePrice = 0m;
var exitAveragePrice = 0m;
var relatedOrderIds = new HashSet<int>();
while (position.PendingFills.Count > 0)
{
var currentFill = position.PendingFills[index];
if (Math.Sign(currentFill.FillQuantity) != Math.Sign(fill.FillQuantity))
{
// entry
totalEntryQuantity += currentFill.FillQuantity;
entryAveragePrice += (currentFill.FillPrice - entryAveragePrice) * currentFill.FillQuantity / totalEntryQuantity;
}
else
{
// exit
totalExitQuantity += currentFill.FillQuantity;
exitAveragePrice += (currentFill.FillPrice - exitAveragePrice) * currentFill.FillQuantity / totalExitQuantity;
}
relatedOrderIds.Add(currentFill.OrderId);
position.PendingFills.RemoveAt(index);
if (_matchingMethod == FillMatchingMethod.LIFO && index > 0) index--;
}
var direction = Math.Sign(fill.FillQuantity) < 0 ? TradeDirection.Long : TradeDirection.Short;
var trade = new Trade
{
Symbols = [fill.Symbol],
EntryTime = entryTime,
EntryPrice = entryAveragePrice,
Direction = direction,
Quantity = Math.Abs(totalEntryQuantity),
ExitTime = fill.UtcTime,
ExitPrice = exitAveragePrice,
ProfitLoss = Math.Round((exitAveragePrice - entryAveragePrice) * Math.Abs(totalEntryQuantity) * Math.Sign(totalEntryQuantity) * conversionRate * multiplier, 2),
TotalFees = position.TotalFees,
OrderIds = relatedOrderIds
// MAE, MFE, EndTradeDrawdown are zero for FlatToFlat grouping method.
// WE can fix this in the future if needed, but it might require tracking market prices
// during the life of the trade, so that we can compute these metrics accurately accounting for
// time, each fill entry price and quantity, which affect profit and drawdown and
// adds complexity and memory overhead.
};
AddNewTrade(trade, fill);
_positions.Remove(fill.Symbol);
if (reverseQuantity != 0)
{
// direction reversal
fill.FillQuantity = reverseQuantity;
_positions[fill.Symbol] = new Position
{
PendingFills = new List<OrderEvent> { fill },
TotalFees = 0,
MinPrice = fill.FillPrice,
MaxPrice = fill.FillPrice
};
}
}
else
{
// trade open
position.PendingFills.Add(fill);
position.TotalFees += orderFee;
}
}
}
private void ProcessFillUsingFlatToReduced(OrderEvent fill, decimal orderFee, decimal conversionRate, decimal multiplier)
{
Position position;
if (!_positions.TryGetValue(fill.Symbol, out position) || position.PendingFills.Count == 0)
{
// no pending executions for symbol
_positions[fill.Symbol] = new Position
{
PendingFills = new List<OrderEvent> { fill },
TotalFees = orderFee,
MinPrice = fill.FillPrice,
MaxPrice = fill.FillPrice
};
return;
}
SetMarketPrice(fill.Symbol, fill.FillPrice);
var index = _matchingMethod == FillMatchingMethod.FIFO ? 0 : position.PendingFills.Count - 1;
if (Math.Sign(fill.FillQuantity) == Math.Sign(position.PendingFills[index].FillQuantity))
{
// execution has same direction of trade
position.PendingFills.Add(fill);
position.TotalFees += orderFee;
}
else
{
// execution has opposite direction of trade
var entryTime = position.PendingFills[index].UtcTime;
var totalExecutedQuantity = 0m;
var entryPrice = 0m;
position.TotalFees += orderFee;
var relatedOrderIds = new HashSet<int> { fill.OrderId };
while (position.PendingFills.Count > 0 && Math.Abs(totalExecutedQuantity) < fill.AbsoluteFillQuantity)
{
var currentFill = position.PendingFills[index];
var absoluteUnexecutedQuantity = fill.AbsoluteFillQuantity - Math.Abs(totalExecutedQuantity);
if (absoluteUnexecutedQuantity >= Math.Abs(currentFill.FillQuantity))
{
if (_matchingMethod == FillMatchingMethod.LIFO)
entryTime = currentFill.UtcTime;
totalExecutedQuantity -= currentFill.FillQuantity;
entryPrice -= (currentFill.FillPrice - entryPrice) * currentFill.FillQuantity / totalExecutedQuantity;
position.PendingFills.RemoveAt(index);
if (_matchingMethod == FillMatchingMethod.LIFO && index > 0) index--;
}
else
{
var executedQuantity = absoluteUnexecutedQuantity * Math.Sign(fill.FillQuantity);
totalExecutedQuantity += executedQuantity;
entryPrice += (currentFill.FillPrice - entryPrice) * executedQuantity / totalExecutedQuantity;
currentFill.FillQuantity += executedQuantity;
}
relatedOrderIds.Add(currentFill.OrderId);
}
var direction = totalExecutedQuantity < 0 ? TradeDirection.Long : TradeDirection.Short;
var trade = new Trade
{
Symbols = [fill.Symbol],
EntryTime = entryTime,
EntryPrice = entryPrice,
Direction = direction,
Quantity = Math.Abs(totalExecutedQuantity),
ExitTime = fill.UtcTime,
ExitPrice = fill.FillPrice,
ProfitLoss = Math.Round((fill.FillPrice - entryPrice) * Math.Abs(totalExecutedQuantity) * Math.Sign(-totalExecutedQuantity) * conversionRate * multiplier, 2),
TotalFees = position.TotalFees,
OrderIds = relatedOrderIds
// MAE, MFE, EndTradeDrawdown are zero for FlatToReduce grouping method.
// See comment in FlatToFlat method for more details.541
};
AddNewTrade(trade, fill);
if (Math.Abs(totalExecutedQuantity) < fill.AbsoluteFillQuantity)
{
// direction reversal
fill.FillQuantity -= totalExecutedQuantity;
position.PendingFills = new List<OrderEvent> { fill };
position.TotalFees = 0;
position.MinPrice = fill.FillPrice;
position.MaxPrice = fill.FillPrice;
}
else if (Math.Abs(totalExecutedQuantity) == fill.AbsoluteFillQuantity)
{
if (position.PendingFills.Count == 0)
_positions.Remove(fill.Symbol);
else
position.TotalFees = 0;
}
}
}
/// <summary>
/// Adds a trade to the list of closed trades, capping the total number only in live mode
/// </summary>
private void AddNewTrade(Trade trade, OrderEvent fill)
{
lock (_closedTrades)
{
trade.IsWin = _securities != null && _securities.TryGetValue(trade.Symbol, out var security)
? fill.IsWin(security, trade.ProfitLoss)
: trade.ProfitLoss > 0;
trade.Id = Guid.NewGuid().ToString();
_closedTrades.Add(trade);
// Due to memory constraints in live mode, we cap the number of trades
if (!_liveMode)
return;
// maximum number of trades
if (_closedTrades.Count > LiveModeMaxTradeCount)
{
_closedTrades.RemoveRange(0, _closedTrades.Count - LiveModeMaxTradeCount);
}
// maximum age of trades
while (_closedTrades.Count > 0 && _closedTrades[0].ExitTime.Date.AddMonths(LiveModeMaxTradeAgeMonths) < DateTime.Today)
{
_closedTrades.RemoveAt(0);
}
}
}
}
}
+70
View File
@@ -0,0 +1,70 @@
/*
* 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.
*/
namespace QuantConnect.Statistics
{
/// <summary>
/// Direction of a trade
/// </summary>
public enum TradeDirection
{
/// <summary>
/// Long direction (0)
/// </summary>
Long,
/// <summary>
/// Short direction (1)
/// </summary>
Short
}
/// <summary>
/// The method used to group order fills into trades
/// </summary>
public enum FillGroupingMethod
{
/// <summary>
/// A Trade is defined by a fill that establishes or increases a position and an offsetting fill that reduces the position size (0)
/// </summary>
FillToFill,
/// <summary>
/// A Trade is defined by a sequence of fills, from a flat position to a non-zero position which may increase or decrease in quantity, and back to a flat position (1)
/// </summary>
FlatToFlat,
/// <summary>
/// A Trade is defined by a sequence of fills, from a flat position to a non-zero position and an offsetting fill that reduces the position size (2)
/// </summary>
FlatToReduced
}
/// <summary>
/// The method used to match offsetting order fills
/// </summary>
public enum FillMatchingMethod
{
/// <summary>
/// First In First Out fill matching method (0)
/// </summary>
FIFO,
/// <summary>
/// Last In Last Out fill matching method (1)
/// </summary>
LIFO
}
}
+443
View File
@@ -0,0 +1,443 @@
/*
* 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 Newtonsoft.Json;
using QuantConnect.Util;
namespace QuantConnect.Statistics
{
/// <summary>
/// The <see cref="TradeStatistics"/> class represents a set of statistics calculated from a list of closed trades
/// </summary>
public class TradeStatistics
{
/// <summary>
/// The entry date/time of the first trade
/// </summary>
public DateTime? StartDateTime { get; set; }
/// <summary>
/// The exit date/time of the last trade
/// </summary>
public DateTime? EndDateTime { get; set; }
/// <summary>
/// The total number of trades
/// </summary>
public int TotalNumberOfTrades { get; set; }
/// <summary>
/// The total number of winning trades
/// </summary>
public int NumberOfWinningTrades { get; set; }
/// <summary>
/// The total number of losing trades
/// </summary>
public int NumberOfLosingTrades { get; set; }
/// <summary>
/// The total profit/loss for all trades (as symbol currency)
/// </summary>
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal TotalProfitLoss { get; set; }
/// <summary>
/// The total profit for all winning trades (as symbol currency)
/// </summary>
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal TotalProfit { get; set; }
/// <summary>
/// The total loss for all losing trades (as symbol currency)
/// </summary>
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal TotalLoss { get; set; }
/// <summary>
/// The largest profit in a single trade (as symbol currency)
/// </summary>
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal LargestProfit { get; set; }
/// <summary>
/// The largest loss in a single trade (as symbol currency)
/// </summary>
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal LargestLoss { get; set; }
/// <summary>
/// The average profit/loss (a.k.a. Expectancy or Average Trade) for all trades (as symbol currency)
/// </summary>
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal AverageProfitLoss { get; set; }
/// <summary>
/// The average profit for all winning trades (as symbol currency)
/// </summary>
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal AverageProfit { get; set; }
/// <summary>
/// The average loss for all winning trades (as symbol currency)
/// </summary>
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal AverageLoss { get; set; }
/// <summary>
/// The average duration for all trades
/// </summary>
public TimeSpan AverageTradeDuration { get; set; }
/// <summary>
/// The average duration for all winning trades
/// </summary>
public TimeSpan AverageWinningTradeDuration { get; set; }
/// <summary>
/// The average duration for all losing trades
/// </summary>
public TimeSpan AverageLosingTradeDuration { get; set; }
/// <summary>
/// The median duration for all trades
/// </summary>
public TimeSpan MedianTradeDuration { get; set; }
/// <summary>
/// The median duration for all winning trades
/// </summary>
public TimeSpan MedianWinningTradeDuration { get; set; }
/// <summary>
/// The median duration for all losing trades
/// </summary>
public TimeSpan MedianLosingTradeDuration { get; set; }
/// <summary>
/// The maximum number of consecutive winning trades
/// </summary>
public int MaxConsecutiveWinningTrades { get; set; }
/// <summary>
/// The maximum number of consecutive losing trades
/// </summary>
public int MaxConsecutiveLosingTrades { get; set; }
/// <summary>
/// The ratio of the average profit per trade to the average loss per trade
/// </summary>
/// <remarks>If the average loss is zero, ProfitLossRatio is set to 0</remarks>
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal ProfitLossRatio { get; set; }
/// <summary>
/// The ratio of the number of winning trades to the number of losing trades
/// </summary>
/// <remarks>If the total number of trades is zero, WinLossRatio is set to zero</remarks>
/// <remarks>If the number of losing trades is zero and the number of winning trades is nonzero, WinLossRatio is set to 10</remarks>
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal WinLossRatio { get; set; }
/// <summary>
/// The ratio of the number of trades with positive profit loss to the total number of trades
/// </summary>
/// <remarks>If the total number of trades is zero, WinRate is set to zero</remarks>
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal WinRate { get; set; }
/// <summary>
/// The ratio of the number of trades with zero or negative profit loss to the total number of trades
/// </summary>
/// <remarks>If the total number of trades is zero, LossRate is set to zero</remarks>
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal LossRate { get; set; }
/// <summary>
/// The average Maximum Adverse Excursion for all trades
/// </summary>
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal AverageMAE { get; set; }
/// <summary>
/// The average Maximum Favorable Excursion for all trades
/// </summary>
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal AverageMFE { get; set; }
/// <summary>
/// The largest Maximum Adverse Excursion in a single trade (as symbol currency)
/// </summary>
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal LargestMAE { get; set; }
/// <summary>
/// The largest Maximum Favorable Excursion in a single trade (as symbol currency)
/// </summary>
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal LargestMFE { get; set; }
/// <summary>
/// The maximum closed-trade drawdown for all trades (as symbol currency)
/// </summary>
/// <remarks>The calculation only takes into account the profit/loss of each trade</remarks>
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal MaximumClosedTradeDrawdown { get; set; }
/// <summary>
/// The maximum intra-trade drawdown for all trades (as symbol currency)
/// </summary>
/// <remarks>The calculation takes into account MAE and MFE of each trade</remarks>
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal MaximumIntraTradeDrawdown { get; set; }
/// <summary>
/// The standard deviation of the profits/losses for all trades (as symbol currency)
/// </summary>
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal ProfitLossStandardDeviation { get; set; }
/// <summary>
/// The downside deviation of the profits/losses for all trades (as symbol currency)
/// </summary>
/// <remarks>This metric only considers deviations of losing trades</remarks>
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal ProfitLossDownsideDeviation { get; set; }
/// <summary>
/// The ratio of the total profit to the total loss
/// </summary>
/// <remarks>If the total profit is zero, ProfitFactor is set to zero</remarks>
/// <remarks>if the total loss is zero and the total profit is nonzero, ProfitFactor is set to 10</remarks>
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal ProfitFactor { get; set; }
/// <summary>
/// The ratio of the average profit/loss to the standard deviation
/// </summary>
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal SharpeRatio { get; set; }
/// <summary>
/// The ratio of the average profit/loss to the downside deviation
/// </summary>
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal SortinoRatio { get; set; }
/// <summary>
/// The ratio of the total profit/loss to the maximum closed trade drawdown
/// </summary>
/// <remarks>If the total profit/loss is zero, ProfitToMaxDrawdownRatio is set to zero</remarks>
/// <remarks>if the drawdown is zero and the total profit is nonzero, ProfitToMaxDrawdownRatio is set to 10</remarks>
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal ProfitToMaxDrawdownRatio { get; set; }
/// <summary>
/// The maximum amount of profit given back by a single trade before exit (as symbol currency)
/// </summary>
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal MaximumEndTradeDrawdown { get; set; }
/// <summary>
/// The average amount of profit given back by all trades before exit (as symbol currency)
/// </summary>
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal AverageEndTradeDrawdown { get; set; }
/// <summary>
/// The maximum amount of time to recover from a drawdown (longest time between new equity highs or peaks)
/// </summary>
public TimeSpan MaximumDrawdownDuration { get; set; }
/// <summary>
/// The sum of fees for all trades
/// </summary>
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal TotalFees { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="TradeStatistics"/> class
/// </summary>
/// <param name="trades">The list of closed trades</param>
public TradeStatistics(IEnumerable<Trade> trades)
{
var maxConsecutiveWinners = 0;
var maxConsecutiveLosers = 0;
var maxTotalProfitLoss = 0m;
var maxTotalProfitLossWithMfe = 0m;
var sumForVariance = 0m;
var sumForDownsideVariance = 0m;
var lastPeakTime = DateTime.MinValue;
var isInDrawdown = false;
var allTradeDurationsTicks = new List<long>();
var winningTradeDurationsTicks = new List<long>();
var losingTradeDurationsTicks = new List<long>();
var numberOfITMOptionsWinningTrades = 0;
foreach (var trade in trades)
{
if (lastPeakTime == DateTime.MinValue) lastPeakTime = trade.EntryTime;
if (StartDateTime == null || trade.EntryTime < StartDateTime)
StartDateTime = trade.EntryTime;
if (EndDateTime == null || trade.ExitTime > EndDateTime)
EndDateTime = trade.ExitTime;
TotalNumberOfTrades++;
if (TotalProfitLoss + trade.MFE > maxTotalProfitLossWithMfe)
maxTotalProfitLossWithMfe = TotalProfitLoss + trade.MFE;
if (TotalProfitLoss + trade.MAE - maxTotalProfitLossWithMfe < MaximumIntraTradeDrawdown)
MaximumIntraTradeDrawdown = TotalProfitLoss + trade.MAE - maxTotalProfitLossWithMfe;
if (trade.ProfitLoss > 0)
{
// winning trade
NumberOfWinningTrades++;
TotalProfitLoss += trade.ProfitLoss;
TotalProfit += trade.ProfitLoss;
AverageProfit += (trade.ProfitLoss - AverageProfit) / NumberOfWinningTrades;
AverageWinningTradeDuration += TimeSpan.FromSeconds((trade.Duration.TotalSeconds - AverageWinningTradeDuration.TotalSeconds) / NumberOfWinningTrades);
winningTradeDurationsTicks.Add(trade.Duration.Ticks);
if (trade.ProfitLoss > LargestProfit)
LargestProfit = trade.ProfitLoss;
maxConsecutiveWinners++;
maxConsecutiveLosers = 0;
if (maxConsecutiveWinners > MaxConsecutiveWinningTrades)
MaxConsecutiveWinningTrades = maxConsecutiveWinners;
if (TotalProfitLoss > maxTotalProfitLoss)
{
// new equity high
maxTotalProfitLoss = TotalProfitLoss;
if (isInDrawdown && trade.ExitTime - lastPeakTime > MaximumDrawdownDuration)
MaximumDrawdownDuration = trade.ExitTime - lastPeakTime;
lastPeakTime = trade.ExitTime;
isInDrawdown = false;
}
}
else
{
// losing trade
NumberOfLosingTrades++;
TotalProfitLoss += trade.ProfitLoss;
TotalLoss += trade.ProfitLoss;
var prevAverageLoss = AverageLoss;
AverageLoss += (trade.ProfitLoss - AverageLoss) / NumberOfLosingTrades;
sumForDownsideVariance += (trade.ProfitLoss - prevAverageLoss) * (trade.ProfitLoss - AverageLoss);
var downsideVariance = NumberOfLosingTrades > 1 ? sumForDownsideVariance / (NumberOfLosingTrades - 1) : 0;
ProfitLossDownsideDeviation = (decimal)Math.Sqrt((double)downsideVariance);
AverageLosingTradeDuration += TimeSpan.FromSeconds((trade.Duration.TotalSeconds - AverageLosingTradeDuration.TotalSeconds) / NumberOfLosingTrades);
losingTradeDurationsTicks.Add(trade.Duration.Ticks);
if (trade.ProfitLoss < LargestLoss)
LargestLoss = trade.ProfitLoss;
// even though losing money, an ITM option trade is a winning trade,
// so IsWin for an ITM OptionTrade will return true even if the trade was not profitable.
if (trade.IsWin)
{
numberOfITMOptionsWinningTrades++;
maxConsecutiveLosers = 0;
maxConsecutiveWinners++;
if (maxConsecutiveWinners > MaxConsecutiveWinningTrades)
MaxConsecutiveWinningTrades = maxConsecutiveWinners;
}
else
{
maxConsecutiveWinners = 0;
maxConsecutiveLosers++;
if (maxConsecutiveLosers > MaxConsecutiveLosingTrades)
MaxConsecutiveLosingTrades = maxConsecutiveLosers;
}
if (TotalProfitLoss - maxTotalProfitLoss < MaximumClosedTradeDrawdown)
MaximumClosedTradeDrawdown = TotalProfitLoss - maxTotalProfitLoss;
isInDrawdown = true;
}
var prevAverageProfitLoss = AverageProfitLoss;
AverageProfitLoss += (trade.ProfitLoss - AverageProfitLoss) / TotalNumberOfTrades;
sumForVariance += (trade.ProfitLoss - prevAverageProfitLoss) * (trade.ProfitLoss - AverageProfitLoss);
var variance = TotalNumberOfTrades > 1 ? sumForVariance / (TotalNumberOfTrades - 1) : 0;
ProfitLossStandardDeviation = (decimal)Math.Sqrt((double)variance);
AverageTradeDuration += TimeSpan.FromSeconds((trade.Duration.TotalSeconds - AverageTradeDuration.TotalSeconds) / TotalNumberOfTrades);
allTradeDurationsTicks.Add(trade.Duration.Ticks);
AverageMAE += (trade.MAE - AverageMAE) / TotalNumberOfTrades;
AverageMFE += (trade.MFE - AverageMFE) / TotalNumberOfTrades;
if (trade.MAE < LargestMAE)
LargestMAE = trade.MAE;
if (trade.MFE > LargestMFE)
LargestMFE = trade.MFE;
if (trade.EndTradeDrawdown > MaximumEndTradeDrawdown)
MaximumEndTradeDrawdown = trade.EndTradeDrawdown;
TotalFees += trade.TotalFees;
}
// Adjust number of winning and losing trades: ITM options assignment loss counts as a loss for profit and loss calculations,
// but adds a win to the wins count since this is an actual win even though premium paid is a loss.
NumberOfWinningTrades += numberOfITMOptionsWinningTrades;
NumberOfLosingTrades -= numberOfITMOptionsWinningTrades;
ProfitLossRatio = AverageLoss == 0 ? 0 : AverageProfit / Math.Abs(AverageLoss);
WinLossRatio = TotalNumberOfTrades == 0 ? 0 : (NumberOfLosingTrades > 0 ? (decimal)NumberOfWinningTrades / NumberOfLosingTrades : 10);
WinRate = TotalNumberOfTrades > 0 ? (decimal)NumberOfWinningTrades / TotalNumberOfTrades : 0;
LossRate = TotalNumberOfTrades > 0 ? 1 - WinRate : 0;
ProfitFactor = TotalProfit == 0 ? 0 : (TotalLoss < 0 ? TotalProfit / Math.Abs(TotalLoss) : 10);
SharpeRatio = ProfitLossStandardDeviation > 0 ? AverageProfitLoss / ProfitLossStandardDeviation : 0;
SortinoRatio = ProfitLossDownsideDeviation > 0 ? AverageProfitLoss / ProfitLossDownsideDeviation : 0;
ProfitToMaxDrawdownRatio = TotalProfitLoss == 0 ? 0 : (MaximumClosedTradeDrawdown < 0 ? TotalProfitLoss / Math.Abs(MaximumClosedTradeDrawdown) : 10);
AverageEndTradeDrawdown = AverageProfitLoss - AverageMFE;
if (allTradeDurationsTicks.Count > 0)
MedianTradeDuration = TimeSpan.FromTicks(allTradeDurationsTicks.Median());
if (winningTradeDurationsTicks.Count > 0)
MedianWinningTradeDuration = TimeSpan.FromTicks(winningTradeDurationsTicks.Median());
if (losingTradeDurationsTicks.Count > 0)
MedianLosingTradeDuration = TimeSpan.FromTicks(losingTradeDurationsTicks.Median());
}
/// <summary>
/// Initializes a new instance of the <see cref="TradeStatistics"/> class
/// </summary>
public TradeStatistics()
{
}
}
}