From f4c1f2937d715308616f9a6642168ca5b3ae8909 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Wed, 28 Jan 2026 14:15:41 -0400 Subject: [PATCH 1/4] Fix trade drawdown calculation --- Common/Statistics/Trade.cs | 5 +- Common/Statistics/TradeBuilder.cs | 149 ++++++++++---- Tests/Common/Statistics/TradeBuilderTests.cs | 200 +++++++++++++++++++ 3 files changed, 312 insertions(+), 42 deletions(-) diff --git a/Common/Statistics/Trade.cs b/Common/Statistics/Trade.cs index 35ef2a35c6bb..7fce09eaeae9 100644 --- a/Common/Statistics/Trade.cs +++ b/Common/Statistics/Trade.cs @@ -119,10 +119,7 @@ public TimeSpan Duration /// /// Returns the amount of profit given back before the trade was closed /// - public decimal EndTradeDrawdown - { - get { return ProfitLoss - MFE; } - } + public decimal EndTradeDrawdown { get; set; } /// /// Returns whether the trade was profitable (is a win) or not (a loss) diff --git a/Common/Statistics/TradeBuilder.cs b/Common/Statistics/TradeBuilder.cs index b0b0cb010c30..8cc319f9e47a 100644 --- a/Common/Statistics/TradeBuilder.cs +++ b/Common/Statistics/TradeBuilder.cs @@ -13,14 +13,14 @@ * limitations under the License. */ -using System; -using System.Collections.Generic; -using System.Linq; 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 { @@ -29,20 +29,35 @@ namespace QuantConnect.Statistics /// public class TradeBuilder : ITradeBuilder { + private interface IDrawdownTracker + { + decimal MaxProfit { get; set; } + decimal MaxDrawdown { get; set; } + } + + private class TradeState : IDrawdownTracker + { + internal Trade Trade { get; set; } + public decimal MaxProfit { get; set; } + public decimal MaxDrawdown { get; set; } + } + /// /// Helper class to manage pending trades and market price updates for a symbol /// - private class Position + private class Position : IDrawdownTracker { - internal List PendingTrades { get; set; } + internal List PendingTrades { get; set; } internal List PendingFills { get; set; } internal decimal TotalFees { get; set; } internal decimal MaxPrice { get; set; } internal decimal MinPrice { get; set; } + public decimal MaxProfit { get; set; } + public decimal MaxDrawdown { get; set; } public Position() { - PendingTrades = new List(); + PendingTrades = new List(); PendingFills = new List(); } } @@ -130,6 +145,23 @@ public void SetMarketPrice(Symbol symbol, decimal price) position.MaxPrice = price; else if (price < position.MinPrice) position.MinPrice = price; + + if (_groupingMethod == FillGroupingMethod.FillToFill) + { + foreach (var tradeState in position.PendingTrades) + { + var trade = tradeState.Trade; + var currentProfit = trade.Direction == TradeDirection.Long ? price - trade.EntryPrice : trade.EntryPrice - price; + UpdateDrawdownState(tradeState, currentProfit); + } + } + else if (position.PendingFills.Count > 0) + { + var currentProfit = position.PendingFills[0].FillQuantity > 0 + ? price - position.PendingFills[0].FillPrice + : position.PendingFills[0].FillPrice - price; + UpdateDrawdownState(position, currentProfit); + } } /// @@ -150,12 +182,16 @@ public void ApplySplit(Split split, bool liveMode, DataNormalizationMode dataNor position.MinPrice *= split.SplitFactor; position.MaxPrice *= split.SplitFactor; + position.MaxProfit *= split.SplitFactor; + position.MaxDrawdown *= split.SplitFactor; - foreach (var trade in position.PendingTrades) + foreach (var tradeState in position.PendingTrades) { - trade.Quantity /= split.SplitFactor; - trade.EntryPrice *= split.SplitFactor; - trade.ExitPrice *= split.SplitFactor; + 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) @@ -223,17 +259,20 @@ private void ProcessFillUsingFillToFill(OrderEvent fill, decimal orderFee, decim // no pending trades for symbol _positions[fill.Symbol] = new Position { - PendingTrades = new List + PendingTrades = new List { - new Trade + new TradeState { - 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() { fill.OrderId } + 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() { fill.OrderId } + } } }, MinPrice = fill.FillPrice, @@ -246,18 +285,21 @@ private void ProcessFillUsingFillToFill(OrderEvent fill, decimal orderFee, decim var index = _matchingMethod == FillMatchingMethod.FIFO ? 0 : position.PendingTrades.Count - 1; - if (Math.Sign(fill.FillQuantity) == (position.PendingTrades[index].Direction == TradeDirection.Long ? +1 : -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 Trade + position.PendingTrades.Add(new TradeState { - 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() { fill.OrderId } + 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() { fill.OrderId } + } }); } else @@ -267,7 +309,8 @@ private void ProcessFillUsingFillToFill(OrderEvent fill, decimal orderFee, decim var orderFeeAssigned = false; while (position.PendingTrades.Count > 0 && Math.Abs(totalExecutedQuantity) < fill.AbsoluteFillQuantity) { - var trade = position.PendingTrades[index]; + var tradeState = position.PendingTrades[index]; + var trade = tradeState.Trade; var absoluteUnexecutedQuantity = fill.AbsoluteFillQuantity - Math.Abs(totalExecutedQuantity); if (absoluteUnexecutedQuantity >= trade.Quantity) @@ -285,6 +328,7 @@ private void ProcessFillUsingFillToFill(OrderEvent fill, decimal orderFee, decim 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); } @@ -306,6 +350,7 @@ private void ProcessFillUsingFillToFill(OrderEvent fill, decimal orderFee, decim 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([..trade.OrderIds, fill.OrderId]) }; @@ -325,17 +370,20 @@ private void ProcessFillUsingFillToFill(OrderEvent fill, decimal orderFee, decim { // direction reversal fill.FillQuantity -= totalExecutedQuantity; - position.PendingTrades = new List + position.PendingTrades = new List { - new Trade + new TradeState { - 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() { fill.OrderId } + 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() { fill.OrderId } + } } }; position.MinPrice = fill.FillPrice; @@ -423,6 +471,7 @@ private void ProcessFillUsingFlatToFlat(OrderEvent fill, decimal orderFee, decim TotalFees = position.TotalFees, MAE = Math.Round((direction == TradeDirection.Long ? position.MinPrice - entryAveragePrice : entryAveragePrice - position.MaxPrice) * Math.Abs(totalEntryQuantity) * conversionRate * multiplier, 2), MFE = Math.Round((direction == TradeDirection.Long ? position.MaxPrice - entryAveragePrice : entryAveragePrice - position.MinPrice) * Math.Abs(totalEntryQuantity) * conversionRate * multiplier, 2), + EndTradeDrawdown = Math.Round(position.MaxDrawdown * Math.Abs(totalEntryQuantity) * conversionRate * multiplier, 2), OrderIds = relatedOrderIds }; @@ -526,6 +575,7 @@ private void ProcessFillUsingFlatToReduced(OrderEvent fill, decimal orderFee, de TotalFees = position.TotalFees, MAE = Math.Round((direction == TradeDirection.Long ? position.MinPrice - entryPrice : entryPrice - position.MaxPrice) * Math.Abs(totalExecutedQuantity) * conversionRate * multiplier, 2), MFE = Math.Round((direction == TradeDirection.Long ? position.MaxPrice - entryPrice : entryPrice - position.MinPrice) * Math.Abs(totalExecutedQuantity) * conversionRate * multiplier, 2), + EndTradeDrawdown = Math.Round(position.MaxDrawdown * Math.Abs(totalExecutedQuantity) * conversionRate * multiplier, 2), OrderIds = relatedOrderIds }; @@ -539,6 +589,8 @@ private void ProcessFillUsingFlatToReduced(OrderEvent fill, decimal orderFee, de position.TotalFees = 0; position.MinPrice = fill.FillPrice; position.MaxPrice = fill.FillPrice; + position.MaxProfit = 0; + position.MaxDrawdown = 0; } else if (Math.Abs(totalExecutedQuantity) == fill.AbsoluteFillQuantity) { @@ -580,5 +632,26 @@ private void AddNewTrade(Trade trade, OrderEvent fill) } } } + + /// + /// Updates the drawdown state given the current profit + /// + private static void UpdateDrawdownState(IDrawdownTracker drawdownTracker, decimal currentProfit) + { + if (currentProfit < drawdownTracker.MaxProfit) + { + // There is a drawdown, but we only care about the maximum drawdown + var drawdown = drawdownTracker.MaxProfit - currentProfit; + if (drawdown > drawdownTracker.MaxDrawdown) + { + drawdownTracker.MaxDrawdown = drawdown; + } + } + else + { + // New maximum profit + drawdownTracker.MaxProfit = currentProfit; + } + } } } diff --git a/Tests/Common/Statistics/TradeBuilderTests.cs b/Tests/Common/Statistics/TradeBuilderTests.cs index d77a5140a7ad..077f692c5515 100644 --- a/Tests/Common/Statistics/TradeBuilderTests.cs +++ b/Tests/Common/Statistics/TradeBuilderTests.cs @@ -14,6 +14,7 @@ */ using System; +using System.Collections.Generic; using NUnit.Framework; using QuantConnect.Data; using QuantConnect.Data.Market; @@ -2704,6 +2705,205 @@ public void OptionPositionCloseWithoutExercise( CollectionAssert.AreEquivalent(new[] { 1, 2 }, trade.OrderIds); } + [TestCaseSource(nameof(DrawdownTestCases))] + public void DrawdownCalculation(PositionSide entrySide, FillGroupingMethod fillGroupingMethod, decimal[] prices, decimal expectedDrawdown) + { + if (prices.Length < 2) + { + Assert.Fail("At least two prices are required to perform the test."); + } + + // Buy 1k, Sell 1k (entrySide == Long) or Sell 1k, Buy 1k (entrySide == Short) + + var builder = new TradeBuilder(fillGroupingMethod, FillMatchingMethod.FIFO); + builder.SetSecurityManager(_securityManager); + var time = _startTime; + + var quantity = (entrySide == PositionSide.Long ? 1 : -1) * 1000m; + + // Open position + builder.ProcessFill( + new OrderEvent(1, Symbols.SPY, time, OrderStatus.Filled, entrySide == PositionSide.Long ? OrderDirection.Buy : OrderDirection.Sell, + fillPrice: prices[0], fillQuantity: quantity, orderFee: _orderFee), + ConversionRate, _orderFee.Value.Amount); + + Assert.IsTrue(builder.HasOpenPosition(Symbols.SPY)); + + for (int i = 1; i < prices.Length - 1; i++) + { + builder.SetMarketPrice(Symbols.SPY, prices[i]); + } + + // Close position + builder.ProcessFill( + new OrderEvent(2, Symbols.SPY, time.AddMinutes(10), OrderStatus.Filled, entrySide == PositionSide.Long ? OrderDirection.Sell : OrderDirection.Buy, + fillPrice: prices[^1], fillQuantity: -quantity, orderFee: _orderFee), + ConversionRate, _orderFee.Value.Amount); + + Assert.IsFalse(builder.HasOpenPosition(Symbols.SPY)); + + Assert.AreEqual(1, builder.ClosedTrades.Count); + + var trade = builder.ClosedTrades[0]; + + Assert.AreEqual(expectedDrawdown * Math.Abs(quantity), trade.EndTradeDrawdown); + } + + private static IEnumerable DrawdownTestCases + { + get + { + foreach (var fillGroupingMethod in new [] { FillGroupingMethod.FillToFill, FillGroupingMethod.FlatToFlat, FillGroupingMethod.FlatToReduced }) + { + // Long trades + // ------------------------------- + + // Price 100 -> 120 -> 110 + // /\ + // / \ + // / ---- + // / + // ---- + // We expect a drawdown of 10 (from 120 to 110) + yield return new TestCaseData(PositionSide.Long, fillGroupingMethod, new[] { 100m, 120m, 110m }, 10m).SetName($"DrawdownLongTrade_SingleDrawdown_{fillGroupingMethod}"); + + // Price 100 -> 140 -> 120 -> 130 -> 110 + // /\ + // / \ + // / \ /\ + // / \/ \ + // / \ + // / \ + // / ---- + // / + // ---- + // We expect a drawdown of 30 (from 140 to 110) + yield return new TestCaseData(PositionSide.Long, fillGroupingMethod, new[] { 100m, 140m, 120m, 130m, 110m }, 30m).SetName($"DrawdownLongTrade_MultipleDrawdownsOnSingleHighestPrice_{fillGroupingMethod}"); + + // Price 100 -> 120 -> 110 -> 120 -> 140 -> 115 + // /\ + // / \ + // / \ + // / \ + // /\ / \ + // / \/ \ + // / \ + // / ---- + // ---- + // We expect a drawdown of 25 (from 140 to 115) + yield return new TestCaseData(PositionSide.Long, fillGroupingMethod, new[] { 100m, 120m, 110m, 120m, 140m, 115m }, 25m).SetName($"DrawdownLongTrade_HighestDrawdownOnNewHighestPrice_{fillGroupingMethod}"); + + // Price 100 -> 120 -> 110 -> 120 -> 130 -> 125 + // /\ + // / ---- + // /\ / + // / \/ + // / + // / + // ---- + // We expect a drawdown of 10 (from 120 to 110) + yield return new TestCaseData(PositionSide.Long, fillGroupingMethod, new[] { 100m, 120m, 110m, 120m, 130m, 125m }, 10m).SetName($"DrawdownLongTrade_LowerDrawdownOnNewHighestPrice_{fillGroupingMethod}"); + + // Price 100 -> 80 -> 110 + // ---- + // / + // ---- / + // \ / + // \ / + // \ / + // \/ + // We expect a drawdown of 20 (from 100 to 80) + yield return new TestCaseData(PositionSide.Long, fillGroupingMethod, new[] { 100m, 80m, 110m }, 20m).SetName($"DrawdownLongTrade_PriceGoesBelowEntryPrice_{fillGroupingMethod}"); + + // Price 100 -> 90 -> 130 -> 110 + // /\ + // / \ + // / \ + // / \ + // / ---- + // / + // ---- / + // \ / + // \/ + // We expect a drawdown of 20 (from 130 to 110 which is higher than the first one from 100 to 90) + yield return new TestCaseData(PositionSide.Long, fillGroupingMethod, new[] { 100m, 90m, 130m, 110m }, 20m).SetName($"DrawdownLongTrade_HigherDrawdownAfterPriceGoesBelowEntryPrice_{fillGroupingMethod}"); + + // Short trades + // ------------------------------- + + // Price 100 -> 80 -> 90 + // ---- + // \ + // \ ---- + // \ / + // \/ + // We expect a drawdown of 10 (from 80 to 90) + yield return new TestCaseData(PositionSide.Short, fillGroupingMethod, new[] { 100m, 80m, 90m }, 10m).SetName($"DrawdownShortTrade_SingleDrawdown_{fillGroupingMethod}"); + + // Price 100 -> 60 -> 80 -> 70 -> 90 + // ---- + // \ + // \ ---- + // \ / + // \ / + // \ /\ / + // \ / \/ + // \ / + // \/ + // We expect a drawdown of 30 (from 60 to 90) + yield return new TestCaseData(PositionSide.Short, fillGroupingMethod, new[] { 100m, 60m, 80m, 70m, 90m }, 30m).SetName($"DrawdownShortTrade_MultipleDrawdownsOnSingleLowestPrice_{fillGroupingMethod}"); + + // Price 100 -> 80 -> 90 -> 80 -> 60 -> 85 + // ---- + // \ ---- + // \ / + // \ /\ / + // \/ \ / + // \ / + // \/ + // We expect a drawdown of 25 (from 60 to 85) + yield return new TestCaseData(PositionSide.Short, fillGroupingMethod, new[] { 100m, 80m, 90m, 80m, 60m, 85m }, 25m).SetName($"DrawdownShortTrade_HighestDrawdownOnNewLowestPrice_{fillGroupingMethod}"); + + // Price 100 -> 80 -> 90 -> 80 -> 70 -> 75 + // ---- + // \ + // \ + // \ /\ + // \/ \ + // \ ---- + // \/ + + // We expect a drawdown of 10 (from 80 to 90) + yield return new TestCaseData(PositionSide.Short, fillGroupingMethod, new[] { 100m, 80m, 90m, 80m, 70m, 75m }, 10m).SetName($"DrawdownShortTrade_LowerDrawdownOnNewLowestPrice_{fillGroupingMethod}"); + + // Price 100 -> 120 -> 90 + // /\ + // / \ + // / \ + // / \ + // ---- \ + // \ + // \ + // ---- + // We expect a drawdown of 20 (from 100 to 120) + yield return new TestCaseData(PositionSide.Short, fillGroupingMethod, new[] { 100m, 120m, 90m }, 20m).SetName($"DrawdownShortTrade_PriceGoesAboveEntryPrice_{fillGroupingMethod}"); + + // Price 100 -> 110 -> 70 -> 90 + // /\ + // / \ + // ---- \ + // \ + // \ ---- + // \ / + // \ / + // \ / + // \/ + // We expect a drawdown of 20 (from 70 to 90 which is higher than the first one from 100 to 110) + yield return new TestCaseData(PositionSide.Short, fillGroupingMethod, new[] { 100m, 110m, 70m, 90m }, 20m).SetName($"DrawdownShortTrade_HigherDrawdownAfterPriceGoesAboveEntryPrice_{fillGroupingMethod}"); + } + } + } + private Option GetOption() { var underlying = new Security( From 93b4e4ba1f56fbd95914a0c8856b887fdec6c0e2 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Thu, 29 Jan 2026 11:38:03 -0400 Subject: [PATCH 2/4] Cleanup --- Common/Statistics/TradeBuilder.cs | 63 +++++++++++++++---------------- 1 file changed, 31 insertions(+), 32 deletions(-) diff --git a/Common/Statistics/TradeBuilder.cs b/Common/Statistics/TradeBuilder.cs index 8cc319f9e47a..84b5173add9b 100644 --- a/Common/Statistics/TradeBuilder.cs +++ b/Common/Statistics/TradeBuilder.cs @@ -29,31 +29,51 @@ namespace QuantConnect.Statistics /// public class TradeBuilder : ITradeBuilder { - private interface IDrawdownTracker + /// + /// Helper class to track trades maximum drawdown + /// + private abstract class DrawdownTracker { - decimal MaxProfit { get; set; } - decimal MaxDrawdown { get; set; } + internal decimal MaxProfit { get; set; } + internal decimal MaxDrawdown { get; set; } + + /// + /// Updates the drawdown state given the current profit + /// + 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; + } + } } - private class TradeState : IDrawdownTracker + private class TradeState : DrawdownTracker { internal Trade Trade { get; set; } - public decimal MaxProfit { get; set; } - public decimal MaxDrawdown { get; set; } } /// /// Helper class to manage pending trades and market price updates for a symbol /// - private class Position : IDrawdownTracker + private class Position : DrawdownTracker { internal List PendingTrades { get; set; } internal List PendingFills { get; set; } internal decimal TotalFees { get; set; } internal decimal MaxPrice { get; set; } internal decimal MinPrice { get; set; } - public decimal MaxProfit { get; set; } - public decimal MaxDrawdown { get; set; } public Position() { @@ -152,7 +172,7 @@ public void SetMarketPrice(Symbol symbol, decimal price) { var trade = tradeState.Trade; var currentProfit = trade.Direction == TradeDirection.Long ? price - trade.EntryPrice : trade.EntryPrice - price; - UpdateDrawdownState(tradeState, currentProfit); + tradeState.UpdateDrawdown(currentProfit); } } else if (position.PendingFills.Count > 0) @@ -160,7 +180,7 @@ public void SetMarketPrice(Symbol symbol, decimal price) var currentProfit = position.PendingFills[0].FillQuantity > 0 ? price - position.PendingFills[0].FillPrice : position.PendingFills[0].FillPrice - price; - UpdateDrawdownState(position, currentProfit); + position.UpdateDrawdown(currentProfit); } } @@ -632,26 +652,5 @@ private void AddNewTrade(Trade trade, OrderEvent fill) } } } - - /// - /// Updates the drawdown state given the current profit - /// - private static void UpdateDrawdownState(IDrawdownTracker drawdownTracker, decimal currentProfit) - { - if (currentProfit < drawdownTracker.MaxProfit) - { - // There is a drawdown, but we only care about the maximum drawdown - var drawdown = drawdownTracker.MaxProfit - currentProfit; - if (drawdown > drawdownTracker.MaxDrawdown) - { - drawdownTracker.MaxDrawdown = drawdown; - } - } - else - { - // New maximum profit - drawdownTracker.MaxProfit = currentProfit; - } - } } } From 18038e584ccd0744cf8b98e581f3b4ef431a6b31 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Fri, 30 Jan 2026 11:42:47 -0400 Subject: [PATCH 3/4] Disable MAE. MFE and Drawdown calculation for FlatToFlat and FlatToReduced trade grouping methods --- Common/Statistics/TradeBuilder.cs | 49 +- Tests/Common/Statistics/TradeBuilderTests.cs | 542 +++++++++++-------- 2 files changed, 329 insertions(+), 262 deletions(-) diff --git a/Common/Statistics/TradeBuilder.cs b/Common/Statistics/TradeBuilder.cs index 84b5173add9b..10cd6b9ef3c1 100644 --- a/Common/Statistics/TradeBuilder.cs +++ b/Common/Statistics/TradeBuilder.cs @@ -29,11 +29,9 @@ namespace QuantConnect.Statistics /// public class TradeBuilder : ITradeBuilder { - /// - /// Helper class to track trades maximum drawdown - /// - private abstract class DrawdownTracker + private class TradeState { + internal Trade Trade { get; set; } internal decimal MaxProfit { get; set; } internal decimal MaxDrawdown { get; set; } @@ -59,15 +57,10 @@ public void UpdateDrawdown(decimal currentProfit) } } - private class TradeState : DrawdownTracker - { - internal Trade Trade { get; set; } - } - /// /// Helper class to manage pending trades and market price updates for a symbol /// - private class Position : DrawdownTracker + private class Position { internal List PendingTrades { get; set; } internal List PendingFills { get; set; } @@ -166,21 +159,11 @@ public void SetMarketPrice(Symbol symbol, decimal price) else if (price < position.MinPrice) position.MinPrice = price; - if (_groupingMethod == FillGroupingMethod.FillToFill) - { - foreach (var tradeState in position.PendingTrades) - { - var trade = tradeState.Trade; - var currentProfit = trade.Direction == TradeDirection.Long ? price - trade.EntryPrice : trade.EntryPrice - price; - tradeState.UpdateDrawdown(currentProfit); - } - } - else if (position.PendingFills.Count > 0) + foreach (var tradeState in position.PendingTrades) { - var currentProfit = position.PendingFills[0].FillQuantity > 0 - ? price - position.PendingFills[0].FillPrice - : position.PendingFills[0].FillPrice - price; - position.UpdateDrawdown(currentProfit); + var trade = tradeState.Trade; + var currentProfit = trade.Direction == TradeDirection.Long ? price - trade.EntryPrice : trade.EntryPrice - price; + tradeState.UpdateDrawdown(currentProfit); } } @@ -202,8 +185,6 @@ public void ApplySplit(Split split, bool liveMode, DataNormalizationMode dataNor position.MinPrice *= split.SplitFactor; position.MaxPrice *= split.SplitFactor; - position.MaxProfit *= split.SplitFactor; - position.MaxDrawdown *= split.SplitFactor; foreach (var tradeState in position.PendingTrades) { @@ -489,10 +470,12 @@ private void ProcessFillUsingFlatToFlat(OrderEvent fill, decimal orderFee, decim ExitPrice = exitAveragePrice, ProfitLoss = Math.Round((exitAveragePrice - entryAveragePrice) * Math.Abs(totalEntryQuantity) * Math.Sign(totalEntryQuantity) * conversionRate * multiplier, 2), TotalFees = position.TotalFees, - MAE = Math.Round((direction == TradeDirection.Long ? position.MinPrice - entryAveragePrice : entryAveragePrice - position.MaxPrice) * Math.Abs(totalEntryQuantity) * conversionRate * multiplier, 2), - MFE = Math.Round((direction == TradeDirection.Long ? position.MaxPrice - entryAveragePrice : entryAveragePrice - position.MinPrice) * Math.Abs(totalEntryQuantity) * conversionRate * multiplier, 2), - EndTradeDrawdown = Math.Round(position.MaxDrawdown * Math.Abs(totalEntryQuantity) * conversionRate * multiplier, 2), 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); @@ -593,10 +576,10 @@ private void ProcessFillUsingFlatToReduced(OrderEvent fill, decimal orderFee, de ExitPrice = fill.FillPrice, ProfitLoss = Math.Round((fill.FillPrice - entryPrice) * Math.Abs(totalExecutedQuantity) * Math.Sign(-totalExecutedQuantity) * conversionRate * multiplier, 2), TotalFees = position.TotalFees, - MAE = Math.Round((direction == TradeDirection.Long ? position.MinPrice - entryPrice : entryPrice - position.MaxPrice) * Math.Abs(totalExecutedQuantity) * conversionRate * multiplier, 2), - MFE = Math.Round((direction == TradeDirection.Long ? position.MaxPrice - entryPrice : entryPrice - position.MinPrice) * Math.Abs(totalExecutedQuantity) * conversionRate * multiplier, 2), - EndTradeDrawdown = Math.Round(position.MaxDrawdown * Math.Abs(totalExecutedQuantity) * conversionRate * multiplier, 2), OrderIds = relatedOrderIds + + // MAE, MFE, EndTradeDrawdown are zero for FlatToReduce grouping method. + // See comment in FlatToFlat method for more details.541 }; AddNewTrade(trade, fill); @@ -609,8 +592,6 @@ private void ProcessFillUsingFlatToReduced(OrderEvent fill, decimal orderFee, de position.TotalFees = 0; position.MinPrice = fill.FillPrice; position.MaxPrice = fill.FillPrice; - position.MaxProfit = 0; - position.MaxDrawdown = 0; } else if (Math.Abs(totalExecutedQuantity) == fill.AbsoluteFillQuantity) { diff --git a/Tests/Common/Statistics/TradeBuilderTests.cs b/Tests/Common/Statistics/TradeBuilderTests.cs index 077f692c5515..6f4590100e4c 100644 --- a/Tests/Common/Statistics/TradeBuilderTests.cs +++ b/Tests/Common/Statistics/TradeBuilderTests.cs @@ -93,8 +93,16 @@ public void AllInAllOutLong( Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade.ExitPrice); Assert.AreEqual(10, trade.ProfitLoss); Assert.AreEqual(2, trade.TotalFees); - Assert.AreEqual(-5, trade.MAE); - Assert.AreEqual(20m, trade.MFE); + if (groupingMethod == FillGroupingMethod.FillToFill) + { + Assert.AreEqual(-5, trade.MAE); + Assert.AreEqual(20m, trade.MFE); + } + else + { + Assert.AreEqual(0, trade.MAE); + Assert.AreEqual(0, trade.MFE); + } CollectionAssert.AreEquivalent(new[] { 1, 2 }, trade.OrderIds); } @@ -151,8 +159,16 @@ public void AllInAllOutShort( Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade.ExitPrice); Assert.AreEqual(-10, trade.ProfitLoss); Assert.AreEqual(2, trade.TotalFees); - Assert.AreEqual(-20, trade.MAE); - Assert.AreEqual(5, trade.MFE); + if (groupingMethod == FillGroupingMethod.FillToFill) + { + Assert.AreEqual(-20, trade.MAE); + Assert.AreEqual(5, trade.MFE); + } + else + { + Assert.AreEqual(0, trade.MAE); + Assert.AreEqual(0, trade.MFE); + } CollectionAssert.AreEquivalent(new[] { 1, 2 }, trade.OrderIds); } @@ -256,8 +272,8 @@ public void ScaleInAllOutLong( Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade.ExitPrice); Assert.AreEqual(30, trade.ProfitLoss); Assert.AreEqual(3, trade.TotalFees); - Assert.AreEqual(-20, trade.MAE); - Assert.AreEqual(50, trade.MFE); + Assert.AreEqual(0, trade.MAE); + Assert.AreEqual(0, trade.MFE); CollectionAssert.AreEquivalent(new[] { 1, 2, 3 }, trade.OrderIds); } } @@ -361,8 +377,8 @@ public void ScaleInAllOutShort( Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade.ExitPrice); Assert.AreEqual(-30, trade.ProfitLoss); Assert.AreEqual(3, trade.TotalFees); - Assert.AreEqual(-50, trade.MAE); - Assert.AreEqual(20, trade.MFE); + Assert.AreEqual(0, trade.MAE); + Assert.AreEqual(0, trade.MFE); CollectionAssert.AreEquivalent(new[] { 1, 2, 3 }, trade.OrderIds); } } @@ -432,8 +448,8 @@ public void AllInScaleOutLong( Assert.AreEqual(AdjustPriceToSplit(1.085m, split), trade.ExitPrice); Assert.AreEqual(30, trade.ProfitLoss); Assert.AreEqual(3, trade.TotalFees); - Assert.AreEqual(-10, trade.MAE); - Assert.AreEqual(60, trade.MFE); + Assert.AreEqual(0, trade.MAE); + Assert.AreEqual(0, trade.MFE); CollectionAssert.AreEquivalent(new[] { 1, 2, 3 }, trade.OrderIds); } else @@ -452,8 +468,16 @@ public void AllInScaleOutLong( Assert.AreEqual(1.08m, trade1.ExitPrice); Assert.AreEqual(10, trade1.ProfitLoss); Assert.AreEqual(2, trade1.TotalFees); - Assert.AreEqual(0, trade1.MAE); - Assert.AreEqual(10, trade1.MFE); + if (groupingMethod == FillGroupingMethod.FillToFill) + { + Assert.AreEqual(0, trade1.MAE); + Assert.AreEqual(10, trade1.MFE); + } + else + { + Assert.AreEqual(0, trade1.MAE); + Assert.AreEqual(0, trade1.MFE); + } CollectionAssert.AreEquivalent(new[] { 1, 2 }, trade1.OrderIds); var trade2 = builder.ClosedTrades[1]; @@ -467,8 +491,16 @@ public void AllInScaleOutLong( Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade2.ExitPrice); Assert.AreEqual(20, trade2.ProfitLoss); Assert.AreEqual(1, trade2.TotalFees); - Assert.AreEqual(-5, trade2.MAE); - Assert.AreEqual(30, trade2.MFE); + if (groupingMethod == FillGroupingMethod.FillToFill) + { + Assert.AreEqual(-5, trade2.MAE); + Assert.AreEqual(30, trade2.MFE); + } + else + { + Assert.AreEqual(0, trade2.MAE); + Assert.AreEqual(0, trade2.MFE); + } CollectionAssert.AreEquivalent(groupingMethod == FillGroupingMethod.FlatToReduced ? [1, 3] : new[] { 1, 3 }, trade2.OrderIds); } } @@ -538,8 +570,8 @@ public void AllInScaleOutShort( Assert.AreEqual(AdjustPriceToSplit(1.085m, split), trade.ExitPrice); Assert.AreEqual(-30, trade.ProfitLoss); Assert.AreEqual(3, trade.TotalFees); - Assert.AreEqual(-60, trade.MAE); - Assert.AreEqual(10, trade.MFE); + Assert.AreEqual(0, trade.MAE); + Assert.AreEqual(0, trade.MFE); CollectionAssert.AreEquivalent(new[] { 1, 2, 3 }, trade.OrderIds); } else @@ -558,8 +590,16 @@ public void AllInScaleOutShort( Assert.AreEqual(1.08m, trade1.ExitPrice); Assert.AreEqual(-10, trade1.ProfitLoss); Assert.AreEqual(2, trade1.TotalFees); - Assert.AreEqual(-10, trade1.MAE); - Assert.AreEqual(0, trade1.MFE); + if (groupingMethod == FillGroupingMethod.FillToFill) + { + Assert.AreEqual(-10, trade1.MAE); + Assert.AreEqual(0, trade1.MFE); + } + else + { + Assert.AreEqual(0, trade1.MAE); + Assert.AreEqual(0, trade1.MFE); + } CollectionAssert.AreEquivalent(new[] { 1, 2 }, trade1.OrderIds); var trade2 = builder.ClosedTrades[1]; @@ -573,8 +613,16 @@ public void AllInScaleOutShort( Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade2.ExitPrice); Assert.AreEqual(-20, trade2.ProfitLoss); Assert.AreEqual(1, trade2.TotalFees); - Assert.AreEqual(-30, trade2.MAE); - Assert.AreEqual(5, trade2.MFE); + if (groupingMethod == FillGroupingMethod.FillToFill) + { + Assert.AreEqual(-30, trade2.MAE); + Assert.AreEqual(5, trade2.MFE); + } + else + { + Assert.AreEqual(0, trade2.MAE); + Assert.AreEqual(0, trade2.MFE); + } CollectionAssert.AreEquivalent(new[] { 1, 3 }, trade2.OrderIds); } } @@ -643,8 +691,16 @@ public void ReversalLongToShort( Assert.AreEqual(1.08m, trade1.ExitPrice); Assert.AreEqual(10, trade1.ProfitLoss); Assert.AreEqual(2, trade1.TotalFees); - Assert.AreEqual(0, trade1.MAE); - Assert.AreEqual(10, trade1.MFE); + if (groupingMethod == FillGroupingMethod.FillToFill) + { + Assert.AreEqual(0, trade1.MAE); + Assert.AreEqual(10, trade1.MFE); + } + else + { + Assert.AreEqual(0, trade1.MAE); + Assert.AreEqual(0, trade1.MFE); + } CollectionAssert.AreEquivalent(new[] { 1, 2 }, trade1.OrderIds); var trade2 = builder.ClosedTrades[1]; @@ -658,8 +714,16 @@ public void ReversalLongToShort( Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade2.ExitPrice); Assert.AreEqual(-10, trade2.ProfitLoss); Assert.AreEqual(1, trade2.TotalFees); - Assert.AreEqual(-20, trade2.MAE); - Assert.AreEqual(15, trade2.MFE); + if (groupingMethod == FillGroupingMethod.FillToFill) + { + Assert.AreEqual(-20, trade2.MAE); + Assert.AreEqual(15, trade2.MFE); + } + else + { + Assert.AreEqual(0, trade2.MAE); + Assert.AreEqual(0, trade2.MFE); + } CollectionAssert.AreEquivalent(new[] { 2, 3 }, trade2.OrderIds); } @@ -727,8 +791,16 @@ public void ReversalShortToLong( Assert.AreEqual(1.08m, trade1.ExitPrice); Assert.AreEqual(-10, trade1.ProfitLoss); Assert.AreEqual(2, trade1.TotalFees); - Assert.AreEqual(-10, trade1.MAE); - Assert.AreEqual(0, trade1.MFE); + if (groupingMethod == FillGroupingMethod.FillToFill) + { + Assert.AreEqual(-10, trade1.MAE); + Assert.AreEqual(0, trade1.MFE); + } + else + { + Assert.AreEqual(0, trade1.MAE); + Assert.AreEqual(0, trade1.MFE); + } CollectionAssert.AreEquivalent(new[] { 1, 2 }, trade1.OrderIds); var trade2 = builder.ClosedTrades[1]; @@ -742,8 +814,16 @@ public void ReversalShortToLong( Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade2.ExitPrice); Assert.AreEqual(10, trade2.ProfitLoss); Assert.AreEqual(1, trade2.TotalFees); - Assert.AreEqual(-15, trade2.MAE); - Assert.AreEqual(20, trade2.MFE); + if (groupingMethod == FillGroupingMethod.FillToFill) + { + Assert.AreEqual(-15, trade2.MAE); + Assert.AreEqual(20, trade2.MFE); + } + else + { + Assert.AreEqual(0, trade2.MAE); + Assert.AreEqual(0, trade2.MFE); + } CollectionAssert.AreEquivalent(new[] { 2, 3 }, trade2.OrderIds); } @@ -897,8 +977,8 @@ public void ScaleInScaleOut1Long( Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade.ExitPrice); Assert.AreEqual(40, trade.ProfitLoss); Assert.AreEqual(5, trade.TotalFees); - Assert.AreEqual(-35, trade.MAE); - Assert.AreEqual(70, trade.MFE); + Assert.AreEqual(0, trade.MAE); + Assert.AreEqual(0, trade.MFE); CollectionAssert.AreEquivalent(new[] { 1, 2, 3, 4, 5 }, trade.OrderIds); } break; @@ -924,8 +1004,8 @@ public void ScaleInScaleOut1Long( Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade1.ExitPrice); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? 20 : 10, trade1.ProfitLoss); Assert.AreEqual(3, trade1.TotalFees); - Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? -5 : -15, trade1.MAE); - Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? 30 : 20, trade1.MFE); + Assert.AreEqual(0, trade1.MAE); + Assert.AreEqual(0, trade1.MFE); CollectionAssert.AreEquivalent(matchingMethod == FillMatchingMethod.FIFO ? [3, 1] : new[] { 3, 2 }, trade1.OrderIds); var trade2 = builder.ClosedTrades[1]; @@ -945,8 +1025,8 @@ public void ScaleInScaleOut1Long( Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade2.ExitPrice); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? 20 : 30, trade2.ProfitLoss); Assert.AreEqual(2, trade2.TotalFees); - Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? -30 : -20, trade2.MAE); - Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? 40 : 50, trade2.MFE); + Assert.AreEqual(0, trade2.MAE); + Assert.AreEqual(0, trade2.MFE); CollectionAssert.AreEquivalent(matchingMethod == FillMatchingMethod.FIFO ? [2, 4, 5] : new[] { 1, 4, 5 }, trade2.OrderIds); } break; @@ -1099,8 +1179,8 @@ public void ScaleInScaleOut1Short( Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade.ExitPrice); Assert.AreEqual(-40, trade.ProfitLoss); Assert.AreEqual(5, trade.TotalFees); - Assert.AreEqual(-70, trade.MAE); - Assert.AreEqual(35, trade.MFE); + Assert.AreEqual(0, trade.MAE); + Assert.AreEqual(0, trade.MFE); CollectionAssert.AreEquivalent(new[] { 1, 2, 3, 4, 5 }, trade.OrderIds); } break; @@ -1124,8 +1204,8 @@ public void ScaleInScaleOut1Short( Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade1.ExitPrice); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? -20 : -10, trade1.ProfitLoss); Assert.AreEqual(3, trade1.TotalFees); - Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? -30 : -20, trade1.MAE); - Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? 5 : 15, trade1.MFE); + Assert.AreEqual(0, trade1.MAE); + Assert.AreEqual(0, trade1.MFE); CollectionAssert.AreEquivalent(matchingMethod == FillMatchingMethod.FIFO ? [1, 3] : new[] { 2, 3 }, trade1.OrderIds); var trade2 = builder.ClosedTrades[1]; @@ -1143,8 +1223,8 @@ public void ScaleInScaleOut1Short( Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade2.ExitPrice); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? -20 : -30, trade2.ProfitLoss); Assert.AreEqual(2, trade2.TotalFees); - Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? -40 : -50, trade2.MAE); - Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? 30 : 20, trade2.MFE); + Assert.AreEqual(0, trade2.MAE); + Assert.AreEqual(0, trade2.MFE); CollectionAssert.AreEquivalent(matchingMethod == FillMatchingMethod.FIFO ? [2, 4, 5] : new[] { 1, 4, 5 }, trade2.OrderIds); } break; @@ -1337,8 +1417,8 @@ public void ScaleInScaleOut2Long( Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade.ExitPrice); Assert.AreEqual(50, trade.ProfitLoss); Assert.AreEqual(5, trade.TotalFees); - Assert.AreEqual(-50, trade.MAE); - Assert.AreEqual(90, trade.MFE); + Assert.AreEqual(0, trade.MAE); + Assert.AreEqual(0, trade.MFE); CollectionAssert.AreEquivalent(new[] { 1, 2, 3, 4, 5 }, trade.OrderIds); } break; @@ -1362,8 +1442,8 @@ public void ScaleInScaleOut2Long( Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade1.ExitPrice); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? 20 : 10, trade1.ProfitLoss); Assert.AreEqual(3, trade1.TotalFees); - Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? -5 : -15, trade1.MAE); - Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? 30 : 20, trade1.MFE); + Assert.AreEqual(0, trade1.MAE); + Assert.AreEqual(0, trade1.MFE); CollectionAssert.AreEquivalent(matchingMethod == FillMatchingMethod.FIFO ? [1, 3] : new[] { 2, 3 }, trade1.OrderIds); var trade2 = builder.ClosedTrades[1]; @@ -1386,8 +1466,8 @@ public void ScaleInScaleOut2Long( Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade2.ExitPrice); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? 30 : 40, trade2.ProfitLoss); Assert.AreEqual(2, trade2.TotalFees); - Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? -45 : -35, trade2.MAE); - Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? 60 : 70, trade2.MFE); + Assert.AreEqual(0, trade2.MAE); + Assert.AreEqual(0, trade2.MFE); CollectionAssert.AreEquivalent(matchingMethod == FillMatchingMethod.FIFO ? [2, 4, 5] : new[] { 1, 2, 4, 5 }, trade2.OrderIds); } break; @@ -1595,8 +1675,8 @@ public void ScaleInScaleOut2Short( Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade.ExitPrice); Assert.AreEqual(-50, trade.ProfitLoss); Assert.AreEqual(5, trade.TotalFees); - Assert.AreEqual(-90, trade.MAE); - Assert.AreEqual(50, trade.MFE); + Assert.AreEqual(0, trade.MAE); + Assert.AreEqual(0, trade.MFE); CollectionAssert.AreEquivalent(new[] { 1, 2, 3, 4, 5 }, trade.OrderIds); } break; @@ -1620,8 +1700,8 @@ public void ScaleInScaleOut2Short( Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade1.ExitPrice); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? -20 : -10, trade1.ProfitLoss); Assert.AreEqual(3, trade1.TotalFees); - Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? -30 : -20, trade1.MAE); - Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? 5 : 15, trade1.MFE); + Assert.AreEqual(0, trade1.MAE); + Assert.AreEqual(0, trade1.MFE); CollectionAssert.AreEquivalent(matchingMethod == FillMatchingMethod.FIFO ? [3, 1] : new[] { 2, 3 }, trade1.OrderIds); var trade2 = builder.ClosedTrades[1]; @@ -1644,8 +1724,8 @@ public void ScaleInScaleOut2Short( Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade2.ExitPrice); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? -30 : -40, trade2.ProfitLoss); Assert.AreEqual(2, trade2.TotalFees); - Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? -60 : -70, trade2.MAE); - Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? 45 : 35, trade2.MFE); + Assert.AreEqual(0, trade2.MAE); + Assert.AreEqual(0, trade2.MFE); CollectionAssert.AreEquivalent(matchingMethod == FillMatchingMethod.FIFO ? [2, 4, 5] : new[] { 1, 2, 4, 5 }, trade2.OrderIds); } break; @@ -1803,8 +1883,8 @@ public void ScaleInScaleOut3Long( Assert.AreEqual(AdjustPriceToSplit(1.095m, split), trade.ExitPrice); Assert.AreEqual(60, trade.ProfitLoss); Assert.AreEqual(6, trade.TotalFees); - Assert.AreEqual(-60, trade.MAE); - Assert.AreEqual(80, trade.MFE); + Assert.AreEqual(0, trade.MAE); + Assert.AreEqual(0, trade.MFE); CollectionAssert.AreEquivalent(new[] { 1, 2, 3, 4, 5, 6 }, trade.OrderIds); } break; @@ -1828,8 +1908,8 @@ public void ScaleInScaleOut3Long( Assert.AreEqual(AdjustPriceToSplit(1.10m, split), trade1.ExitPrice); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? 50 : 30, trade1.ProfitLoss); Assert.AreEqual(4, trade1.TotalFees); - Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? -20 : -40, trade1.MAE); - Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? 50 : 30, trade1.MFE); + Assert.AreEqual(0, trade1.MAE); + Assert.AreEqual(0, trade1.MFE); CollectionAssert.AreEquivalent(matchingMethod == FillMatchingMethod.FIFO ? [1, 2, 4] : new[] { 2, 3, 4 }, trade1.OrderIds); var trade2 = builder.ClosedTrades[1]; @@ -1847,8 +1927,8 @@ public void ScaleInScaleOut3Long( Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade2.ExitPrice); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? 10 : 30, trade2.ProfitLoss); Assert.AreEqual(2, trade2.TotalFees); - Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? -40 : -20, trade2.MAE); - Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? 30 : 50, trade2.MFE); + Assert.AreEqual(0, trade2.MAE); + Assert.AreEqual(0, trade2.MFE); CollectionAssert.AreEquivalent(matchingMethod == FillMatchingMethod.FIFO ? [3, 5, 6] : new[] { 1, 5, 6 }, trade2.OrderIds); } break; @@ -2024,8 +2104,8 @@ public void ScaleInScaleOut3Short( Assert.AreEqual(AdjustPriceToSplit(1.095m, split), trade.ExitPrice); Assert.AreEqual(-60, trade.ProfitLoss); Assert.AreEqual(6, trade.TotalFees); - Assert.AreEqual(-80, trade.MAE); - Assert.AreEqual(60, trade.MFE); + Assert.AreEqual(0, trade.MAE); + Assert.AreEqual(0, trade.MFE); CollectionAssert.AreEquivalent(new[] { 1, 2, 3, 4, 5, 6 }, trade.OrderIds); } break; @@ -2049,8 +2129,8 @@ public void ScaleInScaleOut3Short( Assert.AreEqual(AdjustPriceToSplit(1.10m, split), trade1.ExitPrice); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? -50 : -30, trade1.ProfitLoss); Assert.AreEqual(4, trade1.TotalFees); - Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? -50 : -30, trade1.MAE); - Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? 20 : 40, trade1.MFE); + Assert.AreEqual(0, trade1.MAE); + Assert.AreEqual(0, trade1.MFE); CollectionAssert.AreEquivalent(matchingMethod == FillMatchingMethod.FIFO ? [1, 2, 4] : new[] { 2, 3, 4 }, trade1.OrderIds); var trade2 = builder.ClosedTrades[1]; @@ -2068,8 +2148,8 @@ public void ScaleInScaleOut3Short( Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade2.ExitPrice); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? -10 : -30, trade2.ProfitLoss); Assert.AreEqual(2, trade2.TotalFees); - Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? -30 : -50, trade2.MAE); - Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? 40 : 20, trade2.MFE); + Assert.AreEqual(0, trade2.MAE); + Assert.AreEqual(0, trade2.MFE); CollectionAssert.AreEquivalent(matchingMethod == FillMatchingMethod.FIFO ? [3, 5, 6] : new[] { 1, 5, 6 }, trade2.OrderIds); } break; @@ -2214,8 +2294,8 @@ public void ScaleInScaleOut4Long( Assert.AreEqual(AdjustPriceToSplit(1.0925m, split), trade.ExitPrice); Assert.AreEqual(35, trade.ProfitLoss); Assert.AreEqual(4, trade.TotalFees); - Assert.AreEqual(-20, trade.MAE); - Assert.AreEqual(50, trade.MFE); + Assert.AreEqual(0, trade.MAE); + Assert.AreEqual(0, trade.MFE); CollectionAssert.AreEquivalent(new[] { 1, 2, 3, 4 }, trade.OrderIds); } break; @@ -2237,8 +2317,8 @@ public void ScaleInScaleOut4Long( Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade1.ExitPrice); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? 25 : 20, trade1.ProfitLoss); Assert.AreEqual(3, trade1.TotalFees); - Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? -12.5 : -17.5, trade1.MAE); - Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? 40 : 35, trade1.MFE); + Assert.AreEqual(0, trade1.MAE); + Assert.AreEqual(0, trade1.MFE); CollectionAssert.AreEquivalent(matchingMethod == FillMatchingMethod.FIFO ? [1, 2, 3] : new[] { 1, 2, 3 }, trade1.OrderIds); var trade2 = builder.ClosedTrades[1]; @@ -2256,8 +2336,8 @@ public void ScaleInScaleOut4Long( Assert.AreEqual(AdjustPriceToSplit(1.10m, split), trade2.ExitPrice); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? 10 : 15, trade2.ProfitLoss); Assert.AreEqual(1, trade2.TotalFees); - Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? -7.5 : -2.5, trade2.MAE); - Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? 10 : 15, trade2.MFE); + Assert.AreEqual(0, trade2.MAE); + Assert.AreEqual(0, trade2.MFE); CollectionAssert.AreEquivalent(matchingMethod == FillMatchingMethod.FIFO ? [2, 4] : new[] { 1, 4 }, trade2.OrderIds); } break; @@ -2402,8 +2482,8 @@ public void ScaleInScaleOut4Short( Assert.AreEqual(AdjustPriceToSplit(1.0925m, split), trade.ExitPrice); Assert.AreEqual(-35, trade.ProfitLoss); Assert.AreEqual(4, trade.TotalFees); - Assert.AreEqual(-50, trade.MAE); - Assert.AreEqual(20, trade.MFE); + Assert.AreEqual(0, trade.MAE); + Assert.AreEqual(0, trade.MFE); CollectionAssert.AreEquivalent(new[] { 1, 2, 3, 4 }, trade.OrderIds); } break; @@ -2425,8 +2505,8 @@ public void ScaleInScaleOut4Short( Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade1.ExitPrice); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? -25 : -20, trade1.ProfitLoss); Assert.AreEqual(3, trade1.TotalFees); - Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? -40 : -35, trade1.MAE); - Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? 12.5 : 17.5, trade1.MFE); + Assert.AreEqual(0, trade1.MAE); + Assert.AreEqual(0, trade1.MFE); CollectionAssert.AreEquivalent(matchingMethod == FillMatchingMethod.FIFO ? [1, 2, 3] : new[] { 1, 2, 3 }, trade1.OrderIds); var trade2 = builder.ClosedTrades[1]; @@ -2444,8 +2524,8 @@ public void ScaleInScaleOut4Short( Assert.AreEqual(AdjustPriceToSplit(1.10m, split), trade2.ExitPrice); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? -10 : -15, trade2.ProfitLoss); Assert.AreEqual(1, trade2.TotalFees); - Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? -10 : -15, trade2.MAE); - Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? 7.5 : 2.5, trade2.MFE); + Assert.AreEqual(0, trade2.MAE); + Assert.AreEqual(0, trade2.MFE); CollectionAssert.AreEquivalent(matchingMethod == FillMatchingMethod.FIFO ? [2, 4] : new[] { 1, 4 }, trade2.OrderIds); } break; @@ -2507,8 +2587,16 @@ public void AllInAllOutLongWithMultiplier( Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade.ExitPrice); Assert.AreEqual(10 * multiplier, trade.ProfitLoss); Assert.AreEqual(2, trade.TotalFees); - Assert.AreEqual(-5 * multiplier, trade.MAE); - Assert.AreEqual(20m * multiplier, trade.MFE); + if (groupingMethod == FillGroupingMethod.FillToFill) + { + Assert.AreEqual(-5 * multiplier, trade.MAE); + Assert.AreEqual(20m * multiplier, trade.MFE); + } + else + { + Assert.AreEqual(0, trade.MAE); + Assert.AreEqual(0, trade.MFE); + } CollectionAssert.AreEquivalent(new[] { 1, 2 }, trade.OrderIds); } @@ -2706,7 +2794,7 @@ public void OptionPositionCloseWithoutExercise( } [TestCaseSource(nameof(DrawdownTestCases))] - public void DrawdownCalculation(PositionSide entrySide, FillGroupingMethod fillGroupingMethod, decimal[] prices, decimal expectedDrawdown) + public void DrawdownCalculation(PositionSide entrySide, decimal[] prices, decimal expectedDrawdown) { if (prices.Length < 2) { @@ -2715,7 +2803,7 @@ public void DrawdownCalculation(PositionSide entrySide, FillGroupingMethod fillG // Buy 1k, Sell 1k (entrySide == Long) or Sell 1k, Buy 1k (entrySide == Short) - var builder = new TradeBuilder(fillGroupingMethod, FillMatchingMethod.FIFO); + var builder = new TradeBuilder(FillGroupingMethod.FillToFill, FillMatchingMethod.FIFO); builder.SetSecurityManager(_securityManager); var time = _startTime; @@ -2753,154 +2841,152 @@ private static IEnumerable DrawdownTestCases { get { - foreach (var fillGroupingMethod in new [] { FillGroupingMethod.FillToFill, FillGroupingMethod.FlatToFlat, FillGroupingMethod.FlatToReduced }) - { - // Long trades - // ------------------------------- - - // Price 100 -> 120 -> 110 - // /\ - // / \ - // / ---- - // / - // ---- - // We expect a drawdown of 10 (from 120 to 110) - yield return new TestCaseData(PositionSide.Long, fillGroupingMethod, new[] { 100m, 120m, 110m }, 10m).SetName($"DrawdownLongTrade_SingleDrawdown_{fillGroupingMethod}"); - - // Price 100 -> 140 -> 120 -> 130 -> 110 - // /\ - // / \ - // / \ /\ - // / \/ \ - // / \ - // / \ - // / ---- - // / - // ---- - // We expect a drawdown of 30 (from 140 to 110) - yield return new TestCaseData(PositionSide.Long, fillGroupingMethod, new[] { 100m, 140m, 120m, 130m, 110m }, 30m).SetName($"DrawdownLongTrade_MultipleDrawdownsOnSingleHighestPrice_{fillGroupingMethod}"); - - // Price 100 -> 120 -> 110 -> 120 -> 140 -> 115 - // /\ - // / \ - // / \ - // / \ - // /\ / \ - // / \/ \ - // / \ - // / ---- - // ---- - // We expect a drawdown of 25 (from 140 to 115) - yield return new TestCaseData(PositionSide.Long, fillGroupingMethod, new[] { 100m, 120m, 110m, 120m, 140m, 115m }, 25m).SetName($"DrawdownLongTrade_HighestDrawdownOnNewHighestPrice_{fillGroupingMethod}"); - - // Price 100 -> 120 -> 110 -> 120 -> 130 -> 125 - // /\ - // / ---- - // /\ / - // / \/ - // / - // / - // ---- - // We expect a drawdown of 10 (from 120 to 110) - yield return new TestCaseData(PositionSide.Long, fillGroupingMethod, new[] { 100m, 120m, 110m, 120m, 130m, 125m }, 10m).SetName($"DrawdownLongTrade_LowerDrawdownOnNewHighestPrice_{fillGroupingMethod}"); - - // Price 100 -> 80 -> 110 - // ---- - // / - // ---- / - // \ / - // \ / - // \ / - // \/ - // We expect a drawdown of 20 (from 100 to 80) - yield return new TestCaseData(PositionSide.Long, fillGroupingMethod, new[] { 100m, 80m, 110m }, 20m).SetName($"DrawdownLongTrade_PriceGoesBelowEntryPrice_{fillGroupingMethod}"); - - // Price 100 -> 90 -> 130 -> 110 - // /\ - // / \ - // / \ - // / \ - // / ---- - // / - // ---- / - // \ / - // \/ - // We expect a drawdown of 20 (from 130 to 110 which is higher than the first one from 100 to 90) - yield return new TestCaseData(PositionSide.Long, fillGroupingMethod, new[] { 100m, 90m, 130m, 110m }, 20m).SetName($"DrawdownLongTrade_HigherDrawdownAfterPriceGoesBelowEntryPrice_{fillGroupingMethod}"); - - // Short trades - // ------------------------------- - - // Price 100 -> 80 -> 90 - // ---- - // \ - // \ ---- - // \ / - // \/ - // We expect a drawdown of 10 (from 80 to 90) - yield return new TestCaseData(PositionSide.Short, fillGroupingMethod, new[] { 100m, 80m, 90m }, 10m).SetName($"DrawdownShortTrade_SingleDrawdown_{fillGroupingMethod}"); - - // Price 100 -> 60 -> 80 -> 70 -> 90 - // ---- - // \ - // \ ---- - // \ / - // \ / - // \ /\ / - // \ / \/ - // \ / - // \/ - // We expect a drawdown of 30 (from 60 to 90) - yield return new TestCaseData(PositionSide.Short, fillGroupingMethod, new[] { 100m, 60m, 80m, 70m, 90m }, 30m).SetName($"DrawdownShortTrade_MultipleDrawdownsOnSingleLowestPrice_{fillGroupingMethod}"); - - // Price 100 -> 80 -> 90 -> 80 -> 60 -> 85 - // ---- - // \ ---- - // \ / - // \ /\ / - // \/ \ / - // \ / - // \/ - // We expect a drawdown of 25 (from 60 to 85) - yield return new TestCaseData(PositionSide.Short, fillGroupingMethod, new[] { 100m, 80m, 90m, 80m, 60m, 85m }, 25m).SetName($"DrawdownShortTrade_HighestDrawdownOnNewLowestPrice_{fillGroupingMethod}"); - - // Price 100 -> 80 -> 90 -> 80 -> 70 -> 75 - // ---- - // \ - // \ - // \ /\ - // \/ \ - // \ ---- - // \/ - - // We expect a drawdown of 10 (from 80 to 90) - yield return new TestCaseData(PositionSide.Short, fillGroupingMethod, new[] { 100m, 80m, 90m, 80m, 70m, 75m }, 10m).SetName($"DrawdownShortTrade_LowerDrawdownOnNewLowestPrice_{fillGroupingMethod}"); - - // Price 100 -> 120 -> 90 - // /\ - // / \ - // / \ - // / \ - // ---- \ - // \ - // \ - // ---- - // We expect a drawdown of 20 (from 100 to 120) - yield return new TestCaseData(PositionSide.Short, fillGroupingMethod, new[] { 100m, 120m, 90m }, 20m).SetName($"DrawdownShortTrade_PriceGoesAboveEntryPrice_{fillGroupingMethod}"); - - // Price 100 -> 110 -> 70 -> 90 - // /\ - // / \ - // ---- \ - // \ - // \ ---- - // \ / - // \ / - // \ / - // \/ - // We expect a drawdown of 20 (from 70 to 90 which is higher than the first one from 100 to 110) - yield return new TestCaseData(PositionSide.Short, fillGroupingMethod, new[] { 100m, 110m, 70m, 90m }, 20m).SetName($"DrawdownShortTrade_HigherDrawdownAfterPriceGoesAboveEntryPrice_{fillGroupingMethod}"); - } + + // Long trades + // ------------------------------- + + // Price 100 -> 120 -> 110 + // /\ + // / \ + // / ---- + // / + // ---- + // We expect a drawdown of 10 (from 120 to 110) + yield return new TestCaseData(PositionSide.Long, new[] { 100m, 120m, 110m }, 10m).SetName($"DrawdownLongTrade_SingleDrawdown"); + + // Price 100 -> 140 -> 120 -> 130 -> 110 + // /\ + // / \ + // / \ /\ + // / \/ \ + // / \ + // / \ + // / ---- + // / + // ---- + // We expect a drawdown of 30 (from 140 to 110) + yield return new TestCaseData(PositionSide.Long, new[] { 100m, 140m, 120m, 130m, 110m }, 30m).SetName($"DrawdownLongTrade_MultipleDrawdownsOnSingleHighestPrice"); + + // Price 100 -> 120 -> 110 -> 120 -> 140 -> 115 + // /\ + // / \ + // / \ + // / \ + // /\ / \ + // / \/ \ + // / \ + // / ---- + // ---- + // We expect a drawdown of 25 (from 140 to 115) + yield return new TestCaseData(PositionSide.Long, new[] { 100m, 120m, 110m, 120m, 140m, 115m }, 25m).SetName($"DrawdownLongTrade_HighestDrawdownOnNewHighestPrice"); + + // Price 100 -> 120 -> 110 -> 120 -> 130 -> 125 + // /\ + // / ---- + // /\ / + // / \/ + // / + // / + // ---- + // We expect a drawdown of 10 (from 120 to 110) + yield return new TestCaseData(PositionSide.Long, new[] { 100m, 120m, 110m, 120m, 130m, 125m }, 10m).SetName($"DrawdownLongTrade_LowerDrawdownOnNewHighestPrice"); + + // Price 100 -> 80 -> 110 + // ---- + // / + // ---- / + // \ / + // \ / + // \ / + // \/ + // We expect a drawdown of 20 (from 100 to 80) + yield return new TestCaseData(PositionSide.Long, new[] { 100m, 80m, 110m }, 20m).SetName($"DrawdownLongTrade_PriceGoesBelowEntryPrice"); + + // Price 100 -> 90 -> 130 -> 110 + // /\ + // / \ + // / \ + // / \ + // / ---- + // / + // ---- / + // \ / + // \/ + // We expect a drawdown of 20 (from 130 to 110 which is higher than the first one from 100 to 90) + yield return new TestCaseData(PositionSide.Long, new[] { 100m, 90m, 130m, 110m }, 20m).SetName($"DrawdownLongTrade_HigherDrawdownAfterPriceGoesBelowEntryPrice"); + + // Short trades + // ------------------------------- + + // Price 100 -> 80 -> 90 + // ---- + // \ + // \ ---- + // \ / + // \/ + // We expect a drawdown of 10 (from 80 to 90) + yield return new TestCaseData(PositionSide.Short, new[] { 100m, 80m, 90m }, 10m).SetName($"DrawdownShortTrade_SingleDrawdown"); + + // Price 100 -> 60 -> 80 -> 70 -> 90 + // ---- + // \ + // \ ---- + // \ / + // \ / + // \ /\ / + // \ / \/ + // \ / + // \/ + // We expect a drawdown of 30 (from 60 to 90) + yield return new TestCaseData(PositionSide.Short, new[] { 100m, 60m, 80m, 70m, 90m }, 30m).SetName($"DrawdownShortTrade_MultipleDrawdownsOnSingleLowestPrice"); + + // Price 100 -> 80 -> 90 -> 80 -> 60 -> 85 + // ---- + // \ ---- + // \ / + // \ /\ / + // \/ \ / + // \ / + // \/ + // We expect a drawdown of 25 (from 60 to 85) + yield return new TestCaseData(PositionSide.Short, new[] { 100m, 80m, 90m, 80m, 60m, 85m }, 25m).SetName($"DrawdownShortTrade_HighestDrawdownOnNewLowestPrice"); + + // Price 100 -> 80 -> 90 -> 80 -> 70 -> 75 + // ---- + // \ + // \ + // \ /\ + // \/ \ + // \ ---- + // \/ + + // We expect a drawdown of 10 (from 80 to 90) + yield return new TestCaseData(PositionSide.Short, new[] { 100m, 80m, 90m, 80m, 70m, 75m }, 10m).SetName($"DrawdownShortTrade_LowerDrawdownOnNewLowestPrice"); + + // Price 100 -> 120 -> 90 + // /\ + // / \ + // / \ + // / \ + // ---- \ + // \ + // \ + // ---- + // We expect a drawdown of 20 (from 100 to 120) + yield return new TestCaseData(PositionSide.Short, new[] { 100m, 120m, 90m }, 20m).SetName($"DrawdownShortTrade_PriceGoesAboveEntryPrice"); + + // Price 100 -> 110 -> 70 -> 90 + // /\ + // / \ + // ---- \ + // \ + // \ ---- + // \ / + // \ / + // \ / + // \/ + // We expect a drawdown of 20 (from 70 to 90 which is higher than the first one from 100 to 110) + yield return new TestCaseData(PositionSide.Short, new[] { 100m, 110m, 70m, 90m }, 20m).SetName($"DrawdownShortTrade_HigherDrawdownAfterPriceGoesAboveEntryPrice"); } } From e0462b52d0d42cbe3a78f5b3adf912441f9dc92b Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Fri, 30 Jan 2026 13:30:30 -0400 Subject: [PATCH 4/4] Minor test fixes --- Common/Statistics/TradeBuilder.cs | 3 +- Common/Statistics/TradeStatistics.cs | 2 +- .../Common/Statistics/TradeStatisticsTests.cs | 59 ++++++++++++------- 3 files changed, 41 insertions(+), 23 deletions(-) diff --git a/Common/Statistics/TradeBuilder.cs b/Common/Statistics/TradeBuilder.cs index 10cd6b9ef3c1..85ee3c2fb788 100644 --- a/Common/Statistics/TradeBuilder.cs +++ b/Common/Statistics/TradeBuilder.cs @@ -159,8 +159,9 @@ public void SetMarketPrice(Symbol symbol, decimal price) else if (price < position.MinPrice) position.MinPrice = price; - foreach (var tradeState in position.PendingTrades) + 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); diff --git a/Common/Statistics/TradeStatistics.cs b/Common/Statistics/TradeStatistics.cs index 63a8f67ee421..79381bcc7ae7 100644 --- a/Common/Statistics/TradeStatistics.cs +++ b/Common/Statistics/TradeStatistics.cs @@ -402,7 +402,7 @@ public TradeStatistics(IEnumerable trades) if (trade.MFE > LargestMFE) LargestMFE = trade.MFE; - if (trade.EndTradeDrawdown < MaximumEndTradeDrawdown) + if (trade.EndTradeDrawdown > MaximumEndTradeDrawdown) MaximumEndTradeDrawdown = trade.EndTradeDrawdown; TotalFees += trade.TotalFees; diff --git a/Tests/Common/Statistics/TradeStatisticsTests.cs b/Tests/Common/Statistics/TradeStatisticsTests.cs index 4d5b15a8e690..6095cfdbdb47 100644 --- a/Tests/Common/Statistics/TradeStatisticsTests.cs +++ b/Tests/Common/Statistics/TradeStatisticsTests.cs @@ -110,7 +110,7 @@ public void ThreeWinners() Assert.AreEqual(2.8867513459481276450914878051m, statistics.SharpeRatio); Assert.AreEqual(0, statistics.SortinoRatio); Assert.AreEqual(10, statistics.ProfitToMaxDrawdownRatio); - Assert.AreEqual(-20, statistics.MaximumEndTradeDrawdown); + Assert.AreEqual(20, statistics.MaximumEndTradeDrawdown); Assert.AreEqual(-16.666666666666666666666666666m, statistics.AverageEndTradeDrawdown); Assert.AreEqual(TimeSpan.Zero, statistics.MaximumDrawdownDuration); Assert.AreEqual(6, statistics.TotalFees); @@ -134,7 +134,8 @@ private IEnumerable CreateThreeWinners() ProfitLoss = 20, TotalFees = TradeFee, MAE = -5, - MFE = 30 + MFE = 30, + EndTradeDrawdown = 10 }, new Trade { @@ -148,7 +149,8 @@ private IEnumerable CreateThreeWinners() ProfitLoss = 20, TotalFees = TradeFee, MAE = -30, - MFE = 40 + MFE = 40, + EndTradeDrawdown = 20 }, new Trade { @@ -162,7 +164,8 @@ private IEnumerable CreateThreeWinners() ProfitLoss = 10, TotalFees = TradeFee, MAE = -15, - MFE = 30 + MFE = 30, + EndTradeDrawdown = 20 } }; } @@ -206,7 +209,7 @@ public void ThreeLosers() Assert.AreEqual(-2.8867513459481276450914878051m, statistics.SharpeRatio); Assert.AreEqual(-2.8867513459481276450914878051m, statistics.SortinoRatio); Assert.AreEqual(-1, statistics.ProfitToMaxDrawdownRatio); - Assert.AreEqual(-50, statistics.MaximumEndTradeDrawdown); + Assert.AreEqual(50, statistics.MaximumEndTradeDrawdown); Assert.AreEqual(-33.333333333333333333333333334m, statistics.AverageEndTradeDrawdown); Assert.AreEqual(TimeSpan.Zero, statistics.MaximumDrawdownDuration); Assert.AreEqual(6, statistics.TotalFees); @@ -230,7 +233,8 @@ private IEnumerable CreateThreeLosers() ProfitLoss = -20, TotalFees = TradeFee, MAE = -30, - MFE = 5 + MFE = 5, + EndTradeDrawdown = 25 }, new Trade { @@ -244,7 +248,8 @@ private IEnumerable CreateThreeLosers() ProfitLoss = -20, TotalFees = TradeFee, MAE = -40, - MFE = 30 + MFE = 30, + EndTradeDrawdown = 50 }, new Trade { @@ -258,7 +263,8 @@ private IEnumerable CreateThreeLosers() ProfitLoss = -10, TotalFees = TradeFee, MAE = -30, - MFE = 15 + MFE = 15, + EndTradeDrawdown = 25 } }; } @@ -302,7 +308,7 @@ public void TwoLosersOneWinner() Assert.AreEqual(-0.5773502691896248623516308943m, statistics.SharpeRatio); Assert.AreEqual(0, statistics.SortinoRatio); Assert.AreEqual(-0.75m, statistics.ProfitToMaxDrawdownRatio); - Assert.AreEqual(-50, statistics.MaximumEndTradeDrawdown); + Assert.AreEqual(50, statistics.MaximumEndTradeDrawdown); Assert.AreEqual(-31.666666666666666666666666666667m, statistics.AverageEndTradeDrawdown); Assert.AreEqual(TimeSpan.Zero, statistics.MaximumDrawdownDuration); Assert.AreEqual(6, statistics.TotalFees); @@ -326,7 +332,8 @@ private IEnumerable CreateTwoLosersOneWinner() ProfitLoss = -20, TotalFees = TradeFee, MAE = -30, - MFE = 5 + MFE = 5, + EndTradeDrawdown = 30 }, new Trade { @@ -340,7 +347,8 @@ private IEnumerable CreateTwoLosersOneWinner() ProfitLoss = -20, TotalFees = TradeFee, MAE = -40, - MFE = 30 + MFE = 30, + EndTradeDrawdown = 50 }, new Trade { @@ -354,7 +362,8 @@ private IEnumerable CreateTwoLosersOneWinner() ProfitLoss = 10, TotalFees = TradeFee, MAE = -15, - MFE = 30 + MFE = 30, + EndTradeDrawdown = 20 } }; } @@ -398,7 +407,7 @@ public void OneWinnerTwoLosers() Assert.AreEqual(-0.5773502691896248623516308943m, statistics.SharpeRatio); Assert.AreEqual(0, statistics.SortinoRatio); Assert.AreEqual(-0.75m, statistics.ProfitToMaxDrawdownRatio); - Assert.AreEqual(-50, statistics.MaximumEndTradeDrawdown); + Assert.AreEqual(50, statistics.MaximumEndTradeDrawdown); Assert.AreEqual(-31.666666666666666666666666666667m, statistics.AverageEndTradeDrawdown); Assert.AreEqual(TimeSpan.Zero, statistics.MaximumDrawdownDuration); Assert.AreEqual(6, statistics.TotalFees); @@ -422,7 +431,8 @@ private IEnumerable CreateOneWinnerTwoLosers() ProfitLoss = 10, TotalFees = TradeFee, MAE = -15, - MFE = 30 + MFE = 30, + EndTradeDrawdown = 20 }, new Trade { @@ -436,7 +446,8 @@ private IEnumerable CreateOneWinnerTwoLosers() ProfitLoss = -20, TotalFees = TradeFee, MAE = -30, - MFE = 5 + MFE = 5, + EndTradeDrawdown = 25 }, new Trade { @@ -450,7 +461,8 @@ private IEnumerable CreateOneWinnerTwoLosers() ProfitLoss = -20, TotalFees = TradeFee, MAE = -40, - MFE = 30 + MFE = 30, + EndTradeDrawdown = 50 } }; } @@ -494,7 +506,7 @@ public void OneLoserTwoWinners() Assert.AreEqual(0.1601281538050873438895842626m, statistics.SharpeRatio); Assert.AreEqual(0, statistics.SortinoRatio); Assert.AreEqual(0.5m, statistics.ProfitToMaxDrawdownRatio); - Assert.AreEqual(-25, statistics.MaximumEndTradeDrawdown); + Assert.AreEqual(25, statistics.MaximumEndTradeDrawdown); Assert.AreEqual(-18.333333333333333333333333334m, statistics.AverageEndTradeDrawdown); Assert.AreEqual(TimeSpan.FromMinutes(40), statistics.MaximumDrawdownDuration); Assert.AreEqual(6, statistics.TotalFees); @@ -518,7 +530,8 @@ private IEnumerable CreateOneLoserTwoWinners() ProfitLoss = -20, TotalFees = TradeFee, MAE = -30, - MFE = 5 + MFE = 5, + EndTradeDrawdown = 25 }, new Trade { @@ -532,7 +545,8 @@ private IEnumerable CreateOneLoserTwoWinners() ProfitLoss = 20, TotalFees = TradeFee, MAE = -40, - MFE = 30 + MFE = 30, + EndTradeDrawdown = 10 }, new Trade { @@ -546,7 +560,8 @@ private IEnumerable CreateOneLoserTwoWinners() ProfitLoss = 10, TotalFees = TradeFee, MAE = -15, - MFE = 30 + MFE = 30, + EndTradeDrawdown = 20 } }; } @@ -604,7 +619,7 @@ public void ITMOptionAssignment([Values] bool win) Assert.AreEqual(0.1053137759214006433027413265m, statistics.SharpeRatio); Assert.AreEqual(0m, statistics.SortinoRatio); Assert.AreEqual(0.35m, statistics.ProfitToMaxDrawdownRatio); - Assert.AreEqual(-80000, statistics.MaximumEndTradeDrawdown); + Assert.AreEqual(80000, statistics.MaximumEndTradeDrawdown); Assert.AreEqual(-40000m, statistics.AverageEndTradeDrawdown); Assert.AreEqual(TimeSpan.FromMinutes(30), statistics.MaximumDrawdownDuration); Assert.AreEqual(4, statistics.TotalFees); @@ -629,6 +644,7 @@ private IEnumerable CreateITMOptionAssignment(bool win) TotalFees = TradeFee, MAE = -80000m, MFE = 0, + EndTradeDrawdown = 80000m, IsWin = win, }, new Trade @@ -644,6 +660,7 @@ private IEnumerable CreateITMOptionAssignment(bool win) TotalFees = TradeFee, MAE = 0, MFE = 108000m, + EndTradeDrawdown = 0m, IsWin = true, }, };