Compare commits

...

4 Commits

Author SHA1 Message Date
Martin-Molinero
31e247689f Allow specifying PositionSize wolverine order properties (#9257) 2026-01-30 16:56:17 -03:00
Roman Yavnikov
297207badb feat: add MappedSynchronizingHistoryProvider base class (#9256)
* feat: add MappedSynchronizingHistoryProvider base class

Introduces an abstract class for history providers that handle symbol mapping and time-aligned data slices. Uses IMapFileProvider to resolve ticker changes, provides an abstract method for mapped history retrieval, and overrides GetHistory to synchronize results. Enables nullable reference types and adds documentation.

* Minor tweaks

---------

Co-authored-by: Martin Molinero <martin.molinero1@gmail.com>
2026-01-30 16:55:10 -03:00
Jhonathan Abreu
ecb8e8da41 Fix trades drawdown calculation (#9249)
* Fix trade drawdown calculation

* Cleanup

* Disable MAE. MFE and Drawdown calculation for FlatToFlat and FlatToReduced trade grouping methods

* Minor test fixes
2026-01-30 15:46:41 -04:00
Martin-Molinero
c6c4c1edec Fix fundamental security direct access timestamp (#9255)
- Fix the date used by fundamental data accessed directly through security.
  Updating regression algorithm asserting behavior
2026-01-30 11:41:24 -03:00
11 changed files with 623 additions and 149 deletions

View File

@@ -80,9 +80,17 @@ namespace QuantConnect.Algorithm.CSharp
throw new RegressionTestException($"Unexpected {nameof(Fundamental)} data count {history[0].Values.Count}, expected 2!");
}
// assert all fundamental API data match
foreach (var ticker in new[] {"AAPL", "SPY"})
{
if (!history[0].TryGetValue(ticker, out var fundamental) || fundamental.Price == 0)
var fundamentalThroughSecurity = Securities[ticker].Fundamentals;
var fundamentalThroughAlgo = Fundamentals(ticker);
if (!history[1].TryGetValue(ticker, out var fundamental) || fundamental.Price == 0
|| fundamentalThroughSecurity.Price != fundamental.Price
|| fundamentalThroughSecurity.EndTime != fundamental.EndTime
|| fundamentalThroughAlgo.Price != fundamental.Price
|| fundamentalThroughAlgo.EndTime != fundamental.EndTime)
{
throw new RegressionTestException($"Unexpected {ticker} fundamental data");
}
@@ -142,7 +150,24 @@ namespace QuantConnect.Algorithm.CSharp
var sortedByPeRatio = sortedByDollarVolume.OrderByDescending(x => x.ValuationRatios.PERatio);
// take the top entries from our sorted collection
var topFine = sortedByPeRatio.Take(NumberOfSymbolsFundamental);
var topFine = sortedByPeRatio.Take(NumberOfSymbolsFundamental).ToArray();
// selection fundamental data should match all other APIs
foreach (var fundamentalPoint in topFine)
{
var symbol = fundamentalPoint.Symbol;
var fundamentalThroughSecurity = Securities.ContainsKey(symbol) ? Securities[symbol].Fundamentals : null;
var fundamentalThroughAlgo = Fundamentals(symbol);
if (fundamentalPoint.Price == 0
|| fundamentalThroughSecurity != null && (fundamentalThroughSecurity.Price != fundamentalPoint.Price
|| fundamentalThroughSecurity.EndTime != fundamentalPoint.EndTime)
|| fundamentalThroughAlgo.Price != fundamentalPoint.Price
|| fundamentalThroughAlgo.EndTime != fundamentalPoint.EndTime)
{
throw new RegressionTestException($"Unexpected {symbol} fundamental data in selection");
}
}
// we need to return only the symbol objects
return topFine.Select(x => x.Symbol);

View File

@@ -3379,7 +3379,7 @@ namespace QuantConnect.Algorithm
[DocumentationAttribute(SecuritiesAndPortfolio)]
public Fundamental Fundamentals(Symbol symbol)
{
return new Fundamental(Time, symbol) { EndTime = Time };
return Fundamental.ForDate(Time, symbol);
}
/// <summary>

View File

@@ -71,6 +71,18 @@ namespace QuantConnect.Data.Fundamental
{
}
/// <summary>
/// Creates a new instance
/// </summary>
/// <param name="time">The current time</param>
/// <param name="symbol">The associated symbol</param>
public static Fundamental ForDate(DateTime time, Symbol symbol)
{
// Important: set EndTime to time so that time is previous day midnight, if we just set time, EndTime would be NEXT day midnight.
// Note: data for T date is available on T+1 date, fundamental selection also handles this, see BaseDataCollectionSubscriptionEnumeratorFactory
return new Fundamental(time, symbol) { EndTime = time };
}
/// <summary>
/// Return the URL string source of the file. This will be converted to a stream
/// </summary>

View File

@@ -25,5 +25,10 @@ namespace QuantConnect.Orders
/// The exchange post fix to apply if any
/// </summary>
public string ExchangePostFix { get; set; }
/// <summary>
/// Can optionally specify the position side in the order direction (buy-to-open, sell-to-close, etc.) instead of the default handling
/// </summary>
public OrderPosition? PositionSide { get; set; }
}
}

View File

@@ -597,7 +597,7 @@ namespace QuantConnect.Securities
{
get
{
return new Fundamental(LocalTime, Symbol);
return Fundamental.ForDate(LocalTime, Symbol);
}
}

View File

@@ -119,10 +119,7 @@ namespace QuantConnect.Statistics
/// <summary>
/// Returns the amount of profit given back before the trade was closed
/// </summary>
public decimal EndTradeDrawdown
{
get { return ProfitLoss - MFE; }
}
public decimal EndTradeDrawdown { get; set; }
/// <summary>
/// Returns whether the trade was profitable (is a win) or not (a loss)

View File

@@ -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,12 +29,40 @@ namespace QuantConnect.Statistics
/// </summary>
public class TradeBuilder : ITradeBuilder
{
private class TradeState
{
internal Trade Trade { get; set; }
internal decimal MaxProfit { get; set; }
internal decimal MaxDrawdown { get; set; }
/// <summary>
/// Updates the drawdown state given the current profit
/// </summary>
public void UpdateDrawdown(decimal currentProfit)
{
if (currentProfit < MaxProfit)
{
// There is a drawdown, but we only care about the maximum drawdown
var drawdown = MaxProfit - currentProfit;
if (drawdown > MaxDrawdown)
{
MaxDrawdown = drawdown;
}
}
else
{
// New maximum profit
MaxProfit = currentProfit;
}
}
}
/// <summary>
/// Helper class to manage pending trades and market price updates for a symbol
/// </summary>
private class Position
{
internal List<Trade> PendingTrades { get; set; }
internal List<TradeState> PendingTrades { get; set; }
internal List<OrderEvent> PendingFills { get; set; }
internal decimal TotalFees { get; set; }
internal decimal MaxPrice { get; set; }
@@ -42,7 +70,7 @@ namespace QuantConnect.Statistics
public Position()
{
PendingTrades = new List<Trade>();
PendingTrades = new List<TradeState>();
PendingFills = new List<OrderEvent>();
}
}
@@ -130,6 +158,14 @@ namespace QuantConnect.Statistics
position.MaxPrice = price;
else if (price < position.MinPrice)
position.MinPrice = price;
for (var i = 0; i < position.PendingTrades.Count; i++)
{
var tradeState = position.PendingTrades[i];
var trade = tradeState.Trade;
var currentProfit = trade.Direction == TradeDirection.Long ? price - trade.EntryPrice : trade.EntryPrice - price;
tradeState.UpdateDrawdown(currentProfit);
}
}
/// <summary>
@@ -151,11 +187,13 @@ namespace QuantConnect.Statistics
position.MinPrice *= split.SplitFactor;
position.MaxPrice *= 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 +261,20 @@ namespace QuantConnect.Statistics
// no pending trades for symbol
_positions[fill.Symbol] = new Position
{
PendingTrades = new List<Trade>
PendingTrades = new List<TradeState>
{
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<int>() { 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<int>() { fill.OrderId }
}
}
},
MinPrice = fill.FillPrice,
@@ -246,18 +287,21 @@ namespace QuantConnect.Statistics
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<int>() { 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<int>() { fill.OrderId }
}
});
}
else
@@ -267,7 +311,8 @@ namespace QuantConnect.Statistics
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 +330,7 @@ namespace QuantConnect.Statistics
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 +352,7 @@ namespace QuantConnect.Statistics
TotalFees = trade.TotalFees + (orderFeeAssigned ? 0 : orderFee),
MAE = Math.Round((trade.Direction == TradeDirection.Long ? position.MinPrice - trade.EntryPrice : trade.EntryPrice - position.MaxPrice) * absoluteUnexecutedQuantity * conversionRate * multiplier, 2),
MFE = Math.Round((trade.Direction == TradeDirection.Long ? position.MaxPrice - trade.EntryPrice : trade.EntryPrice - position.MinPrice) * absoluteUnexecutedQuantity * conversionRate * multiplier, 2),
EndTradeDrawdown = Math.Round(tradeState.MaxDrawdown * absoluteUnexecutedQuantity * conversionRate * multiplier, 2),
OrderIds = new HashSet<int>([..trade.OrderIds, fill.OrderId])
};
@@ -325,17 +372,20 @@ namespace QuantConnect.Statistics
{
// direction reversal
fill.FillQuantity -= totalExecutedQuantity;
position.PendingTrades = new List<Trade>
position.PendingTrades = new List<TradeState>
{
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<int>() { 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<int>() { fill.OrderId }
}
}
};
position.MinPrice = fill.FillPrice;
@@ -421,9 +471,12 @@ namespace QuantConnect.Statistics
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),
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);
@@ -524,9 +577,10 @@ namespace QuantConnect.Statistics
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),
OrderIds = relatedOrderIds
// MAE, MFE, EndTradeDrawdown are zero for FlatToReduce grouping method.
// See comment in FlatToFlat method for more details.541
};
AddNewTrade(trade, fill);

View File

@@ -402,7 +402,7 @@ namespace QuantConnect.Statistics
if (trade.MFE > LargestMFE)
LargestMFE = trade.MFE;
if (trade.EndTradeDrawdown < MaximumEndTradeDrawdown)
if (trade.EndTradeDrawdown > MaximumEndTradeDrawdown)
MaximumEndTradeDrawdown = trade.EndTradeDrawdown;
TotalFees += trade.TotalFees;

View File

@@ -0,0 +1,78 @@
/*
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
* Lean Algorithmic Trading Engine v2.0. Copyright 2026 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 NodaTime;
using System.Linq;
using QuantConnect.Data;
using QuantConnect.Util;
using QuantConnect.Interfaces;
using System.Collections.Generic;
using QuantConnect.Lean.Engine.DataFeeds;
namespace QuantConnect.Lean.Engine.HistoricalData
{
/// <summary>
/// Base class for history providers that resolve symbol mappings
/// and synchronize multiple data streams into time-aligned slices.
/// </summary>
public abstract class MappedSynchronizingHistoryProvider : SynchronizingHistoryProvider
{
/// <summary>
/// Resolves map files to correctly handle current and historical ticker symbols.
/// </summary>
private static readonly Lazy<IMapFileProvider> _mapFileProvider = new(Composer.Instance.GetPart<IMapFileProvider>);
/// <summary>
/// Gets historical data for a single resolved history request.
/// Implementations should assume the symbol is already correctly mapped.
/// </summary>
/// <param name="request">The resolved history request.</param>
/// <returns>The historical data.</returns>
public abstract IEnumerable<BaseData>? GetHistory(HistoryRequest request);
/// <summary>
/// Gets the history for the requested securities
/// </summary>
/// <param name="requests">The historical data requests</param>
/// <param name="sliceTimeZone">The time zone used when time stamping the slice instances</param>
/// <returns>An enumerable of the slices of data covering the span specified in each request</returns>
public override IEnumerable<Slice>? GetHistory(IEnumerable<HistoryRequest> requests, DateTimeZone sliceTimeZone)
{
var subscriptions = new List<Subscription>();
foreach (var request in requests)
{
var history = request
.SplitHistoryRequestWithUpdatedMappedSymbol(_mapFileProvider.Value)
.SelectMany(x => GetHistory(x) ?? []);
var subscription = CreateSubscription(request, history);
if (!subscription.MoveNext())
{
continue;
}
subscriptions.Add(subscription);
}
if (subscriptions.Count == 0)
{
return null;
}
// Ownership of subscription is transferred to CreateSliceEnumerableFromSubscriptions
return CreateSliceEnumerableFromSubscriptions(subscriptions, sliceTimeZone);
}
}
}

View File

@@ -14,6 +14,7 @@
*/
using System;
using System.Collections.Generic;
using NUnit.Framework;
using QuantConnect.Data;
using QuantConnect.Data.Market;
@@ -92,8 +93,16 @@ namespace QuantConnect.Tests.Common.Statistics
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);
}
@@ -150,8 +159,16 @@ namespace QuantConnect.Tests.Common.Statistics
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);
}
@@ -255,8 +272,8 @@ namespace QuantConnect.Tests.Common.Statistics
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);
}
}
@@ -360,8 +377,8 @@ namespace QuantConnect.Tests.Common.Statistics
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);
}
}
@@ -431,8 +448,8 @@ namespace QuantConnect.Tests.Common.Statistics
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
@@ -451,8 +468,16 @@ namespace QuantConnect.Tests.Common.Statistics
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];
@@ -466,8 +491,16 @@ namespace QuantConnect.Tests.Common.Statistics
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);
}
}
@@ -537,8 +570,8 @@ namespace QuantConnect.Tests.Common.Statistics
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
@@ -557,8 +590,16 @@ namespace QuantConnect.Tests.Common.Statistics
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];
@@ -572,8 +613,16 @@ namespace QuantConnect.Tests.Common.Statistics
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);
}
}
@@ -642,8 +691,16 @@ namespace QuantConnect.Tests.Common.Statistics
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];
@@ -657,8 +714,16 @@ namespace QuantConnect.Tests.Common.Statistics
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);
}
@@ -726,8 +791,16 @@ namespace QuantConnect.Tests.Common.Statistics
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];
@@ -741,8 +814,16 @@ namespace QuantConnect.Tests.Common.Statistics
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);
}
@@ -896,8 +977,8 @@ namespace QuantConnect.Tests.Common.Statistics
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;
@@ -923,8 +1004,8 @@ namespace QuantConnect.Tests.Common.Statistics
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];
@@ -944,8 +1025,8 @@ namespace QuantConnect.Tests.Common.Statistics
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;
@@ -1098,8 +1179,8 @@ namespace QuantConnect.Tests.Common.Statistics
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;
@@ -1123,8 +1204,8 @@ namespace QuantConnect.Tests.Common.Statistics
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];
@@ -1142,8 +1223,8 @@ namespace QuantConnect.Tests.Common.Statistics
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;
@@ -1336,8 +1417,8 @@ namespace QuantConnect.Tests.Common.Statistics
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;
@@ -1361,8 +1442,8 @@ namespace QuantConnect.Tests.Common.Statistics
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];
@@ -1385,8 +1466,8 @@ namespace QuantConnect.Tests.Common.Statistics
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;
@@ -1594,8 +1675,8 @@ namespace QuantConnect.Tests.Common.Statistics
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;
@@ -1619,8 +1700,8 @@ namespace QuantConnect.Tests.Common.Statistics
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];
@@ -1643,8 +1724,8 @@ namespace QuantConnect.Tests.Common.Statistics
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;
@@ -1802,8 +1883,8 @@ namespace QuantConnect.Tests.Common.Statistics
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;
@@ -1827,8 +1908,8 @@ namespace QuantConnect.Tests.Common.Statistics
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];
@@ -1846,8 +1927,8 @@ namespace QuantConnect.Tests.Common.Statistics
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;
@@ -2023,8 +2104,8 @@ namespace QuantConnect.Tests.Common.Statistics
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;
@@ -2048,8 +2129,8 @@ namespace QuantConnect.Tests.Common.Statistics
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];
@@ -2067,8 +2148,8 @@ namespace QuantConnect.Tests.Common.Statistics
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;
@@ -2213,8 +2294,8 @@ namespace QuantConnect.Tests.Common.Statistics
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;
@@ -2236,8 +2317,8 @@ namespace QuantConnect.Tests.Common.Statistics
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];
@@ -2255,8 +2336,8 @@ namespace QuantConnect.Tests.Common.Statistics
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;
@@ -2401,8 +2482,8 @@ namespace QuantConnect.Tests.Common.Statistics
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;
@@ -2424,8 +2505,8 @@ namespace QuantConnect.Tests.Common.Statistics
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];
@@ -2443,8 +2524,8 @@ namespace QuantConnect.Tests.Common.Statistics
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;
@@ -2506,8 +2587,16 @@ namespace QuantConnect.Tests.Common.Statistics
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);
}
@@ -2704,6 +2793,203 @@ namespace QuantConnect.Tests.Common.Statistics
CollectionAssert.AreEquivalent(new[] { 1, 2 }, trade.OrderIds);
}
[TestCaseSource(nameof(DrawdownTestCases))]
public void DrawdownCalculation(PositionSide entrySide, 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.FillToFill, 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<TestCaseData> DrawdownTestCases
{
get
{
// 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");
}
}
private Option GetOption()
{
var underlying = new Security(

View File

@@ -110,7 +110,7 @@ namespace QuantConnect.Tests.Common.Statistics
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 @@ namespace QuantConnect.Tests.Common.Statistics
ProfitLoss = 20,
TotalFees = TradeFee,
MAE = -5,
MFE = 30
MFE = 30,
EndTradeDrawdown = 10
},
new Trade
{
@@ -148,7 +149,8 @@ namespace QuantConnect.Tests.Common.Statistics
ProfitLoss = 20,
TotalFees = TradeFee,
MAE = -30,
MFE = 40
MFE = 40,
EndTradeDrawdown = 20
},
new Trade
{
@@ -162,7 +164,8 @@ namespace QuantConnect.Tests.Common.Statistics
ProfitLoss = 10,
TotalFees = TradeFee,
MAE = -15,
MFE = 30
MFE = 30,
EndTradeDrawdown = 20
}
};
}
@@ -206,7 +209,7 @@ namespace QuantConnect.Tests.Common.Statistics
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 @@ namespace QuantConnect.Tests.Common.Statistics
ProfitLoss = -20,
TotalFees = TradeFee,
MAE = -30,
MFE = 5
MFE = 5,
EndTradeDrawdown = 25
},
new Trade
{
@@ -244,7 +248,8 @@ namespace QuantConnect.Tests.Common.Statistics
ProfitLoss = -20,
TotalFees = TradeFee,
MAE = -40,
MFE = 30
MFE = 30,
EndTradeDrawdown = 50
},
new Trade
{
@@ -258,7 +263,8 @@ namespace QuantConnect.Tests.Common.Statistics
ProfitLoss = -10,
TotalFees = TradeFee,
MAE = -30,
MFE = 15
MFE = 15,
EndTradeDrawdown = 25
}
};
}
@@ -302,7 +308,7 @@ namespace QuantConnect.Tests.Common.Statistics
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 @@ namespace QuantConnect.Tests.Common.Statistics
ProfitLoss = -20,
TotalFees = TradeFee,
MAE = -30,
MFE = 5
MFE = 5,
EndTradeDrawdown = 30
},
new Trade
{
@@ -340,7 +347,8 @@ namespace QuantConnect.Tests.Common.Statistics
ProfitLoss = -20,
TotalFees = TradeFee,
MAE = -40,
MFE = 30
MFE = 30,
EndTradeDrawdown = 50
},
new Trade
{
@@ -354,7 +362,8 @@ namespace QuantConnect.Tests.Common.Statistics
ProfitLoss = 10,
TotalFees = TradeFee,
MAE = -15,
MFE = 30
MFE = 30,
EndTradeDrawdown = 20
}
};
}
@@ -398,7 +407,7 @@ namespace QuantConnect.Tests.Common.Statistics
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 @@ namespace QuantConnect.Tests.Common.Statistics
ProfitLoss = 10,
TotalFees = TradeFee,
MAE = -15,
MFE = 30
MFE = 30,
EndTradeDrawdown = 20
},
new Trade
{
@@ -436,7 +446,8 @@ namespace QuantConnect.Tests.Common.Statistics
ProfitLoss = -20,
TotalFees = TradeFee,
MAE = -30,
MFE = 5
MFE = 5,
EndTradeDrawdown = 25
},
new Trade
{
@@ -450,7 +461,8 @@ namespace QuantConnect.Tests.Common.Statistics
ProfitLoss = -20,
TotalFees = TradeFee,
MAE = -40,
MFE = 30
MFE = 30,
EndTradeDrawdown = 50
}
};
}
@@ -494,7 +506,7 @@ namespace QuantConnect.Tests.Common.Statistics
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 @@ namespace QuantConnect.Tests.Common.Statistics
ProfitLoss = -20,
TotalFees = TradeFee,
MAE = -30,
MFE = 5
MFE = 5,
EndTradeDrawdown = 25
},
new Trade
{
@@ -532,7 +545,8 @@ namespace QuantConnect.Tests.Common.Statistics
ProfitLoss = 20,
TotalFees = TradeFee,
MAE = -40,
MFE = 30
MFE = 30,
EndTradeDrawdown = 10
},
new Trade
{
@@ -546,7 +560,8 @@ namespace QuantConnect.Tests.Common.Statistics
ProfitLoss = 10,
TotalFees = TradeFee,
MAE = -15,
MFE = 30
MFE = 30,
EndTradeDrawdown = 20
}
};
}
@@ -604,7 +619,7 @@ namespace QuantConnect.Tests.Common.Statistics
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 @@ namespace QuantConnect.Tests.Common.Statistics
TotalFees = TradeFee,
MAE = -80000m,
MFE = 0,
EndTradeDrawdown = 80000m,
IsWin = win,
},
new Trade
@@ -644,6 +660,7 @@ namespace QuantConnect.Tests.Common.Statistics
TotalFees = TradeFee,
MAE = 0,
MFE = 108000m,
EndTradeDrawdown = 0m,
IsWin = true,
},
};