chore: import upstream snapshot with attribution
This commit is contained in:
@@ -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