/*
* 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
{
///
/// The class represents a set of statistics calculated from a list of closed trades
///
public class TradeStatistics
{
///
/// The entry date/time of the first trade
///
public DateTime? StartDateTime { get; set; }
///
/// The exit date/time of the last trade
///
public DateTime? EndDateTime { get; set; }
///
/// The total number of trades
///
public int TotalNumberOfTrades { get; set; }
///
/// The total number of winning trades
///
public int NumberOfWinningTrades { get; set; }
///
/// The total number of losing trades
///
public int NumberOfLosingTrades { get; set; }
///
/// The total profit/loss for all trades (as symbol currency)
///
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal TotalProfitLoss { get; set; }
///
/// The total profit for all winning trades (as symbol currency)
///
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal TotalProfit { get; set; }
///
/// The total loss for all losing trades (as symbol currency)
///
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal TotalLoss { get; set; }
///
/// The largest profit in a single trade (as symbol currency)
///
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal LargestProfit { get; set; }
///
/// The largest loss in a single trade (as symbol currency)
///
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal LargestLoss { get; set; }
///
/// The average profit/loss (a.k.a. Expectancy or Average Trade) for all trades (as symbol currency)
///
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal AverageProfitLoss { get; set; }
///
/// The average profit for all winning trades (as symbol currency)
///
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal AverageProfit { get; set; }
///
/// The average loss for all winning trades (as symbol currency)
///
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal AverageLoss { get; set; }
///
/// The average duration for all trades
///
public TimeSpan AverageTradeDuration { get; set; }
///
/// The average duration for all winning trades
///
public TimeSpan AverageWinningTradeDuration { get; set; }
///
/// The average duration for all losing trades
///
public TimeSpan AverageLosingTradeDuration { get; set; }
///
/// The median duration for all trades
///
public TimeSpan MedianTradeDuration { get; set; }
///
/// The median duration for all winning trades
///
public TimeSpan MedianWinningTradeDuration { get; set; }
///
/// The median duration for all losing trades
///
public TimeSpan MedianLosingTradeDuration { get; set; }
///
/// The maximum number of consecutive winning trades
///
public int MaxConsecutiveWinningTrades { get; set; }
///
/// The maximum number of consecutive losing trades
///
public int MaxConsecutiveLosingTrades { get; set; }
///
/// The ratio of the average profit per trade to the average loss per trade
///
/// If the average loss is zero, ProfitLossRatio is set to 0
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal ProfitLossRatio { get; set; }
///
/// The ratio of the number of winning trades to the number of losing trades
///
/// If the total number of trades is zero, WinLossRatio is set to zero
/// If the number of losing trades is zero and the number of winning trades is nonzero, WinLossRatio is set to 10
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal WinLossRatio { get; set; }
///
/// The ratio of the number of winning trades to the total number of trades
///
/// If the total number of trades is zero, WinRate is set to zero
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal WinRate { get; set; }
///
/// The ratio of the number of losing trades to the total number of trades
///
/// If the total number of trades is zero, LossRate is set to zero
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal LossRate { get; set; }
///
/// The average Maximum Adverse Excursion for all trades
///
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal AverageMAE { get; set; }
///
/// The average Maximum Favorable Excursion for all trades
///
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal AverageMFE { get; set; }
///
/// The largest Maximum Adverse Excursion in a single trade (as symbol currency)
///
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal LargestMAE { get; set; }
///
/// The largest Maximum Favorable Excursion in a single trade (as symbol currency)
///
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal LargestMFE { get; set; }
///
/// The maximum closed-trade drawdown for all trades (as symbol currency)
///
/// The calculation only takes into account the profit/loss of each trade
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal MaximumClosedTradeDrawdown { get; set; }
///
/// The maximum intra-trade drawdown for all trades (as symbol currency)
///
/// The calculation takes into account MAE and MFE of each trade
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal MaximumIntraTradeDrawdown { get; set; }
///
/// The standard deviation of the profits/losses for all trades (as symbol currency)
///
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal ProfitLossStandardDeviation { get; set; }
///
/// The downside deviation of the profits/losses for all trades (as symbol currency)
///
/// This metric only considers deviations of losing trades
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal ProfitLossDownsideDeviation { get; set; }
///
/// The ratio of the total profit to the total loss
///
/// If the total profit is zero, ProfitFactor is set to zero
/// if the total loss is zero and the total profit is nonzero, ProfitFactor is set to 10
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal ProfitFactor { get; set; }
///
/// The ratio of the average profit/loss to the standard deviation
///
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal SharpeRatio { get; set; }
///
/// The ratio of the average profit/loss to the downside deviation
///
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal SortinoRatio { get; set; }
///
/// The ratio of the total profit/loss to the maximum closed trade drawdown
///
/// If the total profit/loss is zero, ProfitToMaxDrawdownRatio is set to zero
/// if the drawdown is zero and the total profit is nonzero, ProfitToMaxDrawdownRatio is set to 10
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal ProfitToMaxDrawdownRatio { get; set; }
///
/// The maximum amount of profit given back by a single trade before exit (as symbol currency)
///
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal MaximumEndTradeDrawdown { get; set; }
///
/// The average amount of profit given back by all trades before exit (as symbol currency)
///
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal AverageEndTradeDrawdown { get; set; }
///
/// The maximum amount of time to recover from a drawdown (longest time between new equity highs or peaks)
///
public TimeSpan MaximumDrawdownDuration { get; set; }
///
/// The sum of fees for all trades
///
[JsonConverter(typeof(JsonRoundingConverter))]
public decimal TotalFees { get; set; }
///
/// Initializes a new instance of the class
///
/// The list of closed trades
public TradeStatistics(IEnumerable 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();
var winningTradeDurationsTicks = new List();
var losingTradeDurationsTicks = new List();
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());
}
///
/// Initializes a new instance of the class
///
public TradeStatistics()
{
}
}
}