chore: import upstream snapshot with attribution
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
{
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user