From c2930798b21c75a09888821b77e70e1392723a58 Mon Sep 17 00:00:00 2001 From: Martin Molinero Date: Mon, 20 Oct 2025 18:20:31 -0300 Subject: [PATCH 1/3] Minor tweak to cache liquidation fees --- Common/Extensions.cs | 5 ++--- Common/Orders/OrderSizing.cs | 2 +- Common/Securities/SecurityHolding.cs | 20 +++++++++++--------- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/Common/Extensions.cs b/Common/Extensions.cs index 8bace7209c7b..9ecf5dcc33ca 100644 --- a/Common/Extensions.cs +++ b/Common/Extensions.cs @@ -4451,10 +4451,9 @@ public static bool IsCustomDataType(Symbol symbol, Type type) /// Security for which we would like to make a market order /// Quantity of the security we are seeking to trade /// Time the order was placed - /// This out parameter will contain the market order constructed - public static CashAmount GetMarketOrderFees(Security security, decimal quantity, DateTime time, out MarketOrder marketOrder) + public static CashAmount GetMarketOrderFees(Security security, decimal quantity, DateTime time) { - marketOrder = new MarketOrder(security.Symbol, quantity, time); + var marketOrder = new MarketOrder(security.Symbol, quantity, time); return security.FeeModel.GetOrderFee(new OrderFeeParameters(security, marketOrder)).Value; } diff --git a/Common/Orders/OrderSizing.cs b/Common/Orders/OrderSizing.cs index 7fb4bc82aaa3..88e601980add 100644 --- a/Common/Orders/OrderSizing.cs +++ b/Common/Orders/OrderSizing.cs @@ -95,7 +95,7 @@ public static decimal GetUnorderedQuantity(IAlgorithm algorithm, IPortfolioTarge // Adjust the order quantity taking into account the fee's if (accountForFees && security.Symbol.SecurityType == SecurityType.Crypto && quantity > 0) { - var orderFee = Extensions.GetMarketOrderFees(security, quantity, algorithm.UtcTime, out _); + var orderFee = Extensions.GetMarketOrderFees(security, quantity, algorithm.UtcTime); var baseCurrency = ((Crypto)security).BaseCurrency.Symbol; if (baseCurrency == orderFee.Currency) { diff --git a/Common/Securities/SecurityHolding.cs b/Common/Securities/SecurityHolding.cs index 71c0860f2793..ef441fb27a92 100644 --- a/Common/Securities/SecurityHolding.cs +++ b/Common/Securities/SecurityHolding.cs @@ -14,8 +14,6 @@ */ using System; -using QuantConnect.Orders; -using QuantConnect.Orders.Fees; using QuantConnect.Algorithm.Framework.Portfolio; namespace QuantConnect.Securities @@ -478,22 +476,26 @@ public virtual ConvertibleCashAmount GetQuantityValue(decimal quantity, decimal /// Does not use the transaction model for market fills but should. public virtual decimal TotalCloseProfit(bool includeFees = true, decimal? exitPrice = null, decimal? entryPrice = null, decimal? quantity = null) { - var quantityToUse = quantity ?? Quantity; - if (quantityToUse == 0) + var quantityToUse = Quantity; + if (quantity.HasValue) + { + quantityToUse = quantity.Value; + } + else if (!_invested) { return 0; } - // this is in the account currency - var orderFee = Extensions.GetMarketOrderFees(_security, -quantityToUse, _security.LocalTime.ConvertToUtc(_security.Exchange.TimeZone), out var marketOrder); - var feesInAccountCurrency = 0m; if (includeFees) { - feesInAccountCurrency = _currencyConverter.ConvertToAccountCurrency(orderFee).Amount; + // this is in the account currency + var liquidationFees = Extensions.GetMarketOrderFees(_security, -quantityToUse, _security.LocalTime.ConvertToUtc(_security.Exchange.TimeZone)); + feesInAccountCurrency = _currencyConverter.ConvertToAccountCurrency(liquidationFees).Amount; } - var price = marketOrder.Direction == OrderDirection.Sell ? _security.BidPrice : _security.AskPrice; + // if we are long, we would need to sell against the bid + var price = IsLong ? _security.BidPrice : _security.AskPrice; if (price == 0) { // Bid/Ask prices can both be equal to 0. This usually happens when we request our holdings from From c68d1051c9814c3add40b6727a5196d3005fdee9 Mon Sep 17 00:00:00 2001 From: Martin Molinero Date: Mon, 20 Oct 2025 19:51:15 -0300 Subject: [PATCH 2/3] Result handler cleanup --- .../BrokerageConcurrentMessageHandler.cs | 6 -- Engine/Results/BacktestingResultHandler.cs | 52 +++------ Engine/Results/BaseResultsHandler.cs | 101 ++++++++++++++---- Engine/Results/LiveTradingResultHandler.cs | 98 ++--------------- .../Engine/Results/BaseResultsHandlerTests.cs | 4 +- 5 files changed, 104 insertions(+), 157 deletions(-) diff --git a/Brokerages/BrokerageConcurrentMessageHandler.cs b/Brokerages/BrokerageConcurrentMessageHandler.cs index cceb6cb9ac7c..d0b896f47996 100644 --- a/Brokerages/BrokerageConcurrentMessageHandler.cs +++ b/Brokerages/BrokerageConcurrentMessageHandler.cs @@ -172,8 +172,6 @@ private interface ILock : IDisposable { int CurrentWriteCount { get; } - void EnterReadLock(); - void ExitReadLock(); bool TryEnterReadLockImmediately(); @@ -204,8 +202,6 @@ public ReaderWriterLockWrapper() { _lock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion); } - - public void EnterReadLock() => _lock.EnterWriteLock(); public void ExitReadLock() => _lock.ExitWriteLock(); public bool TryEnterReadLockImmediately() => _lock.TryEnterWriteLock(0); public void EnterWriteLock() => _lock.EnterReadLock(); @@ -230,8 +226,6 @@ public MonitorWrapper() _lockObject = new object(); } - public void EnterReadLock() => Monitor.Enter(_lockObject); - public void ExitReadLock() => Monitor.Exit(_lockObject); public bool TryEnterReadLockImmediately() => Monitor.TryEnter(_lockObject); diff --git a/Engine/Results/BacktestingResultHandler.cs b/Engine/Results/BacktestingResultHandler.cs index 8e650045f446..7d6367682c20 100644 --- a/Engine/Results/BacktestingResultHandler.cs +++ b/Engine/Results/BacktestingResultHandler.cs @@ -400,15 +400,11 @@ protected void SendFinalResult() /// Algorithm we're working on. /// Algorithm starting capital for statistics calculations /// While setting the algorithm the backtest result handler. - public virtual void SetAlgorithm(IAlgorithm algorithm, decimal startingPortfolioValue) + public override void SetAlgorithm(IAlgorithm algorithm, decimal startingPortfolioValue) { - Algorithm = algorithm; + base.SetAlgorithm(algorithm, startingPortfolioValue); Algorithm.SetStatisticsService(this); State["Name"] = Algorithm.Name; - StartingPortfolioValue = startingPortfolioValue; - DailyPortfolioValue = StartingPortfolioValue; - CumulativeMaxPortfolioValue = StartingPortfolioValue; - AlgorithmCurrencySymbol = Currencies.GetCurrencySymbol(Algorithm.AccountCurrency); _capacityEstimate = new CapacityEstimate(Algorithm); _progressMonitor = new BacktestProgressMonitor(Algorithm.TimeKeeper, Algorithm.EndDate); @@ -418,21 +414,7 @@ public virtual void SetAlgorithm(IAlgorithm algorithm, decimal startingPortfolio ResamplePeriod = TimeSpan.FromMinutes(resampleMinutes); Log.Trace("BacktestingResultHandler(): Sample Period Set: " + resampleMinutes.ToStringInvariant("00.00")); - //Set the security / market types. - var types = new List(); - foreach (var kvp in Algorithm.Securities) - { - var security = kvp.Value; - - if (!types.Contains(security.Type)) types.Add(security.Type); - } - SecurityType(types); - ConfigureConsoleTextWriter(algorithm); - - // Wire algorithm name and tags updates - algorithm.NameUpdated += (sender, name) => AlgorithmNameUpdated(name); - algorithm.TagsUpdated += (sender, tags) => AlgorithmTagsUpdated(tags); } /// @@ -496,18 +478,6 @@ protected override void AddToLogStore(string message) base.AddToLogStore(messageToLog); } - /// - /// Send list of security asset types the algorithm uses to browser. - /// - public virtual void SecurityType(List types) - { - var packet = new SecurityTypesPacket - { - Types = types - }; - Messages.Enqueue(packet); - } - /// /// Send an error message back to the browser highlighted in red with a stacktrace. /// @@ -743,6 +713,11 @@ public virtual void ProcessSynchronousEvents(bool forceProcess = false) { if (Algorithm == null) return; + var time = Algorithm.UtcTime; + + // Check to see if we should update stored portfolio values + UpdatePortfolioValues(time, forceProcess); + _capacityEstimate.UpdateMarketCapacity(forceProcess); // Invalidate the processed days count so it gets recalculated @@ -751,7 +726,6 @@ public virtual void ProcessSynchronousEvents(bool forceProcess = false) // Update the equity bar UpdateAlgorithmEquity(); - var time = Algorithm.UtcTime; if (time > _nextSample || forceProcess) { //Set next sample time: 4000 samples per backtest @@ -762,14 +736,14 @@ public virtual void ProcessSynchronousEvents(bool forceProcess = false) //Also add the user samples / plots to the result handler tracking: SampleRange(Algorithm.GetChartUpdates()); - } - ProcessAlgorithmLogs(); + ProcessAlgorithmLogs(); - //Set the running statistics: - foreach (var pair in Algorithm.RuntimeStatistics) - { - RuntimeStatistic(pair.Key, pair.Value); + //Set the running statistics: + foreach (var pair in Algorithm.RuntimeStatistics) + { + RuntimeStatistic(pair.Key, pair.Value); + } } } diff --git a/Engine/Results/BaseResultsHandler.cs b/Engine/Results/BaseResultsHandler.cs index 7ae7e356cd38..dc774d2ab1c0 100644 --- a/Engine/Results/BaseResultsHandler.cs +++ b/Engine/Results/BaseResultsHandler.cs @@ -22,7 +22,6 @@ using System.Threading; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; -using QuantConnect.Configuration; using QuantConnect.Data.Market; using QuantConnect.Data.UniverseSelection; using QuantConnect.Indicators; @@ -34,6 +33,7 @@ using QuantConnect.Packets; using QuantConnect.Securities.Positions; using QuantConnect.Statistics; +using QuantConnect.Util; namespace QuantConnect.Lean.Engine.Results { @@ -51,6 +51,10 @@ public abstract class BaseResultsHandler private static readonly TextWriter StandardOut = Console.Out; private static readonly TextWriter StandardError = Console.Error; + private ReferenceWrapper _portfolioValue; + private ReferenceWrapper _benchmarkValue; + private ReferenceWrapper _unrealizedProfit; + private string _hostName; private Bar _currentAlgorithmEquity; @@ -474,6 +478,59 @@ public virtual void Initialize(ResultHandlerInitializeParameters parameters) }; } + /// + /// Set the Algorithm instance for ths result. + /// + /// Algorithm we're working on. + /// Algorithm starting capital for statistics calculations + /// While setting the algorithm the backtest result handler. + public virtual void SetAlgorithm(IAlgorithm algorithm, decimal startingPortfolioValue) + { + Algorithm = algorithm; + AlgorithmCurrencySymbol = Currencies.GetCurrencySymbol(Algorithm.AccountCurrency); + CumulativeMaxPortfolioValue = DailyPortfolioValue = StartingPortfolioValue = startingPortfolioValue; + + _unrealizedProfit = new ReferenceWrapper(0); + _benchmarkValue = new ReferenceWrapper(0); + _portfolioValue = new ReferenceWrapper(startingPortfolioValue); + + SecurityType(Algorithm.Securities.Select(x => x.Key.SecurityType).Distinct().ToList()); + + // Wire algorithm name and tags updates + algorithm.NameUpdated += (sender, name) => AlgorithmNameUpdated(name); + algorithm.TagsUpdated += (sender, tags) => AlgorithmTagsUpdated(tags); + } + + /// + /// Send list of security asset types the algorithm uses to browser. + /// + public virtual void SecurityType(List types) + { + var packet = new SecurityTypesPacket + { + Types = types + }; + Messages.Enqueue(packet); + } + + /// + /// Handles updates to the algorithm's name + /// + /// The new name + public virtual void AlgorithmNameUpdated(string name) + { + Messages.Enqueue(new AlgorithmNameUpdatePacket(AlgorithmId, name)); + } + + /// + /// Handles updates to the algorithm's tags + /// + /// The new tags + public virtual void AlgorithmTagsUpdated(HashSet tags) + { + Messages.Enqueue(new AlgorithmTagsUpdatePacket(AlgorithmId, tags)); + } + /// /// Result handler update method /// @@ -545,7 +602,7 @@ protected decimal GetNetReturn() { //Some users have $0 in their brokerage account / starting cash of $0. Prevent divide by zero errors return StartingPortfolioValue > 0 ? - (Algorithm.Portfolio.TotalPortfolioValue - StartingPortfolioValue) / StartingPortfolioValue + (GetPortfolioValue() - StartingPortfolioValue) / StartingPortfolioValue : 0; } @@ -560,26 +617,14 @@ protected decimal GetNetReturn() /// /// Useful so that live trading implementation can freeze the returned value if there is no user exchange open /// so we ignore extended market hours updates - protected virtual decimal GetPortfolioValue() - { - return Algorithm.Portfolio.TotalPortfolioValue; - } + protected decimal GetPortfolioValue() => _portfolioValue.Value; /// /// Gets the current benchmark value /// /// Useful so that live trading implementation can freeze the returned value if there is no user exchange open /// so we ignore extended market hours updates - /// Time to resolve benchmark value at - protected virtual decimal GetBenchmarkValue(DateTime time) - { - if (Algorithm == null || Algorithm.Benchmark == null) - { - // this could happen if the algorithm exploded mid initialization - return 0; - } - return Algorithm.Benchmark.Evaluate(time).SmartRounding(); - } + protected virtual decimal GetBenchmarkValue() => _benchmarkValue.Value; /// /// Samples portfolio equity, benchmark, and daily performance @@ -588,6 +633,10 @@ protected virtual decimal GetBenchmarkValue(DateTime time) /// Current UTC time in the AlgorithmManager loop public virtual void Sample(DateTime time) { + // Force an update for our values before doing our daily sample + UpdatePortfolioValues(time); + UpdateBenchmarkValue(time); + var currentPortfolioValue = GetPortfolioValue(); var portfolioPerformance = DailyPortfolioValue == 0 ? 0 : Math.Round((currentPortfolioValue - DailyPortfolioValue) * 100 / DailyPortfolioValue, 10); @@ -597,7 +646,7 @@ public virtual void Sample(DateTime time) // Sample all our default charts UpdateAlgorithmEquity(); SampleEquity(time); - SampleBenchmark(time, GetBenchmarkValue(time)); + SampleBenchmark(time, GetBenchmarkValue()); SamplePerformance(time, portfolioPerformance); SampleDrawdown(time, currentPortfolioValue); SampleSalesVolume(time); @@ -846,11 +895,11 @@ protected SortedDictionary GetAlgorithmRuntimeStatistics(Diction runtimeStatistics["Probabilistic Sharpe Ratio"] = "0%"; } - runtimeStatistics["Unrealized"] = AlgorithmCurrencySymbol + Algorithm.Portfolio.TotalUnrealizedProfit.ToStringInvariant("N2"); + runtimeStatistics["Unrealized"] = AlgorithmCurrencySymbol + _unrealizedProfit.Value.ToStringInvariant("N2"); runtimeStatistics["Fees"] = $"-{AlgorithmCurrencySymbol}{Algorithm.Portfolio.TotalFees.ToStringInvariant("N2")}"; runtimeStatistics["Net Profit"] = AlgorithmCurrencySymbol + Algorithm.Portfolio.TotalNetProfit.ToStringInvariant("N2"); runtimeStatistics["Return"] = GetNetReturn().ToStringInvariant("P"); - runtimeStatistics["Equity"] = AlgorithmCurrencySymbol + Algorithm.Portfolio.TotalPortfolioValue.ToStringInvariant("N2"); + runtimeStatistics["Equity"] = AlgorithmCurrencySymbol + GetPortfolioValue().ToStringInvariant("N2"); runtimeStatistics["Holdings"] = AlgorithmCurrencySymbol + Algorithm.Portfolio.TotalHoldingsValue.ToStringInvariant("N2"); runtimeStatistics["Volume"] = AlgorithmCurrencySymbol + Algorithm.Portfolio.TotalSaleVolume.ToStringInvariant("N2"); @@ -1078,5 +1127,19 @@ protected void UpdateAlgorithmEquity() { UpdateAlgorithmEquity(CurrentAlgorithmEquity); } + + protected virtual void UpdatePortfolioValues(DateTime time, bool force = false) + { + _portfolioValue = new ReferenceWrapper(Algorithm?.Portfolio.TotalPortfolioValue ?? 0); + _unrealizedProfit = new ReferenceWrapper(Algorithm?.Portfolio.TotalUnrealizedProfit ?? 0); + } + + protected virtual void UpdateBenchmarkValue(DateTime time, bool force = false) + { + if (Algorithm != null && Algorithm.Benchmark != null) + { + _benchmarkValue = new ReferenceWrapper(Algorithm.Benchmark.Evaluate(time).SmartRounding()); + } + } } } diff --git a/Engine/Results/LiveTradingResultHandler.cs b/Engine/Results/LiveTradingResultHandler.cs index 7b544f55f795..71be6dd23c15 100644 --- a/Engine/Results/LiveTradingResultHandler.cs +++ b/Engine/Results/LiveTradingResultHandler.cs @@ -77,8 +77,6 @@ public class LiveTradingResultHandler : BaseResultsHandler, IResultHandler private bool _sampleChartAlways; private bool _userExchangeIsOpen; - private ReferenceWrapper _portfolioValue; - private ReferenceWrapper _benchmarkValue; private DateTime _lastChartSampleLogicCheck; private readonly Dictionary _exchangeHours; @@ -95,9 +93,6 @@ public LiveTradingResultHandler() _samplePortfolioPeriod = _storeInsightPeriod = TimeSpan.FromMinutes(10); _streamedChartLimit = Config.GetInt("streamed-chart-limit", 12); _streamedChartGroupSize = Config.GetInt("streamed-chart-group-size", 3); - - _portfolioValue = new ReferenceWrapper(0); - _benchmarkValue = new ReferenceWrapper(0); } /// @@ -589,16 +584,6 @@ public void ErrorMessage(string message, string stacktrace = "") AddToLogStore(message + (!string.IsNullOrEmpty(stacktrace) ? ": StackTrace: " + stacktrace : string.Empty)); } - /// - /// Send a list of secutity types that the algorithm trades to the browser to show the market clock - is this market open or closed! - /// - /// List of security types - public void SecurityType(List types) - { - var packet = new SecurityTypesPacket { Types = types }; - Messages.Enqueue(packet); - } - /// /// Send a runtime error back to the users browser and highlight it red. /// @@ -713,23 +698,10 @@ protected void SampleRange(IEnumerable updates) /// /// Algorithm object matching IAlgorithm interface /// Algorithm starting capital for statistics calculations - public virtual void SetAlgorithm(IAlgorithm algorithm, decimal startingPortfolioValue) + public override void SetAlgorithm(IAlgorithm algorithm, decimal startingPortfolioValue) { - Algorithm = algorithm; + base.SetAlgorithm(algorithm, startingPortfolioValue); Algorithm.SetStatisticsService(this); - DailyPortfolioValue = StartingPortfolioValue = startingPortfolioValue; - _portfolioValue = new ReferenceWrapper(startingPortfolioValue); - CumulativeMaxPortfolioValue = StartingPortfolioValue; - AlgorithmCurrencySymbol = Currencies.GetCurrencySymbol(Algorithm.AccountCurrency); - - var types = new List(); - foreach (var kvp in Algorithm.Securities) - { - var security = kvp.Value; - - if (!types.Contains(security.Type)) types.Add(security.Type); - } - SecurityType(types); // we need to forward Console.Write messages to the algorithm's Debug function var debug = new FuncTextWriter(algorithm.Debug); @@ -738,13 +710,8 @@ public virtual void SetAlgorithm(IAlgorithm algorithm, decimal startingPortfolio Console.SetError(error); UpdateAlgorithmStatus(); - - // Wire algorithm name and tags updates - algorithm.NameUpdated += (sender, name) => AlgorithmNameUpdated(name); - algorithm.TagsUpdated += (sender, tags) => AlgorithmTagsUpdated(tags); } - /// /// Send a algorithm status update to the user of the algorithms running state. /// @@ -1063,7 +1030,7 @@ public virtual void ProcessSynchronousEvents(bool forceProcess = false) var time = DateTime.UtcNow; // Check to see if we should update stored portfolio values - UpdatePortfolioValue(time, forceProcess); + UpdatePortfolioValues(time, forceProcess); // Update the equity bar UpdateAlgorithmEquity(); @@ -1177,39 +1144,6 @@ public override void OnSecuritiesChanged(SecurityChanges changes) } } - /// - /// Samples portfolio equity, benchmark, and daily performance - /// - /// Current UTC time in the AlgorithmManager loop - public void Sample(DateTime time) - { - // Force an update for our values before doing our daily sample - UpdatePortfolioValue(time); - UpdateBenchmarkValue(time); - base.Sample(time); - } - - /// - /// Gets the current portfolio value - /// - /// Useful so that live trading implementation can freeze the returned value if there is no user exchange open - /// so we ignore extended market hours updates - protected override decimal GetPortfolioValue() - { - return _portfolioValue.Value; - } - - /// - /// Gets the current benchmark value - /// - /// Useful so that live trading implementation can freeze the returned value if there is no user exchange open - /// so we ignore extended market hours updates - /// Time to resolve benchmark value at - protected override decimal GetBenchmarkValue(DateTime time) - { - return _benchmarkValue.Value; - } - /// /// True if user exchange are open and we should update portfolio and benchmark value /// @@ -1273,20 +1207,20 @@ private void UpdateAlgorithmStatus() } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void UpdateBenchmarkValue(DateTime time, bool force = false) + protected override void UpdateBenchmarkValue(DateTime time, bool force = false) { if (force || UserExchangeIsOpen(time)) { - _benchmarkValue = new ReferenceWrapper(base.GetBenchmarkValue(time)); + base.UpdateBenchmarkValue(time, force); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void UpdatePortfolioValue(DateTime time, bool force = false) + protected override void UpdatePortfolioValues(DateTime time, bool force = false) { if (force || UserExchangeIsOpen(time)) { - _portfolioValue = new ReferenceWrapper(base.GetPortfolioValue()); + base.UpdatePortfolioValues(time, force); } } @@ -1329,23 +1263,5 @@ public void SetSummaryStatistic(string name, string value) { SummaryStatistic(name, value); } - - /// - /// Handles updates to the algorithm's name - /// - /// The new name - public virtual void AlgorithmNameUpdated(string name) - { - Messages.Enqueue(new AlgorithmNameUpdatePacket(AlgorithmId, name)); - } - - /// - /// Handles updates to the algorithm's tags - /// - /// The new tags - public virtual void AlgorithmTagsUpdated(HashSet tags) - { - Messages.Enqueue(new AlgorithmTagsUpdatePacket(AlgorithmId, tags)); - } } } diff --git a/Tests/Engine/Results/BaseResultsHandlerTests.cs b/Tests/Engine/Results/BaseResultsHandlerTests.cs index 1f763e3bcbc2..de1ed7e467a6 100644 --- a/Tests/Engine/Results/BaseResultsHandlerTests.cs +++ b/Tests/Engine/Results/BaseResultsHandlerTests.cs @@ -94,7 +94,7 @@ public void ExposureIsCalculatedEvenWhenPortfolioIsNotInvested(decimal holdingsQ protectedMockResultHandler.Setup("SampleEquity", ItExpr.IsAny()); protectedMockResultHandler.Setup("SampleBenchmark", ItExpr.IsAny(), ItExpr.IsAny()); protectedMockResultHandler - .Setup("GetBenchmarkValue", ItExpr.IsAny()) + .Setup("GetBenchmarkValue") .Returns(0m); protectedMockResultHandler.Setup("SamplePerformance", ItExpr.IsAny(), ItExpr.IsAny()); protectedMockResultHandler.Setup("SampleDrawdown", ItExpr.IsAny(), ItExpr.IsAny()); @@ -144,7 +144,7 @@ public void ExposureIsCalculatedEvenWhenPortfolioIsNotInvested(decimal holdingsQ // BaseResultHandler.Algorithm property accessed once by BaseResultHandler.SampleExposure() // and once by BaseResultHandler.GetPortfolioValue() + 2 for sampling current equity value - protectedMockResultHandler.VerifyGet("Algorithm", Times.Exactly(5)); + protectedMockResultHandler.VerifyGet("Algorithm", Times.Exactly(6)); // Sample should've been called twice, by BaseResultHandler.SampleExposure(), once for the long and once for the short positions protectedMockResultHandler.Verify("Sample", Times.Exactly(2), ItExpr.IsAny(), ItExpr.IsAny(), From 3ad685dba2f640b7d0e015a82f824084889cba53 Mon Sep 17 00:00:00 2001 From: Martin Molinero Date: Tue, 21 Oct 2025 11:17:37 -0300 Subject: [PATCH 3/3] Update pythonnet version to 2.0.49 --- .../QuantConnect.Algorithm.CSharp.csproj | 2 +- .../QuantConnect.Algorithm.Framework.csproj | 2 +- .../QuantConnect.Algorithm.Python.csproj | 2 +- Algorithm/QuantConnect.Algorithm.csproj | 2 +- .../QuantConnect.AlgorithmFactory.csproj | 2 +- Common/QuantConnect.csproj | 2 +- Engine/QuantConnect.Lean.Engine.csproj | 2 +- Indicators/QuantConnect.Indicators.csproj | 2 +- Report/QuantConnect.Report.csproj | 2 +- Research/QuantConnect.Research.csproj | 2 +- Tests/Python/PythonThreadingTests.cs | 99 ------------------- Tests/QuantConnect.Tests.csproj | 2 +- 12 files changed, 11 insertions(+), 110 deletions(-) diff --git a/Algorithm.CSharp/QuantConnect.Algorithm.CSharp.csproj b/Algorithm.CSharp/QuantConnect.Algorithm.CSharp.csproj index b8ef6d4c9b4c..358d8f7d7de5 100644 --- a/Algorithm.CSharp/QuantConnect.Algorithm.CSharp.csproj +++ b/Algorithm.CSharp/QuantConnect.Algorithm.CSharp.csproj @@ -32,7 +32,7 @@ portable - + diff --git a/Algorithm.Framework/QuantConnect.Algorithm.Framework.csproj b/Algorithm.Framework/QuantConnect.Algorithm.Framework.csproj index f593ae295c0e..46d7734a7ba2 100644 --- a/Algorithm.Framework/QuantConnect.Algorithm.Framework.csproj +++ b/Algorithm.Framework/QuantConnect.Algorithm.Framework.csproj @@ -29,7 +29,7 @@ LICENSE - + diff --git a/Algorithm.Python/QuantConnect.Algorithm.Python.csproj b/Algorithm.Python/QuantConnect.Algorithm.Python.csproj index 83f2cff8cad8..27d959076f69 100644 --- a/Algorithm.Python/QuantConnect.Algorithm.Python.csproj +++ b/Algorithm.Python/QuantConnect.Algorithm.Python.csproj @@ -37,7 +37,7 @@ - + diff --git a/Algorithm/QuantConnect.Algorithm.csproj b/Algorithm/QuantConnect.Algorithm.csproj index e282739d4e05..030e18be98c2 100644 --- a/Algorithm/QuantConnect.Algorithm.csproj +++ b/Algorithm/QuantConnect.Algorithm.csproj @@ -29,7 +29,7 @@ LICENSE - + diff --git a/AlgorithmFactory/QuantConnect.AlgorithmFactory.csproj b/AlgorithmFactory/QuantConnect.AlgorithmFactory.csproj index fb53d48b563f..c0d552c13880 100644 --- a/AlgorithmFactory/QuantConnect.AlgorithmFactory.csproj +++ b/AlgorithmFactory/QuantConnect.AlgorithmFactory.csproj @@ -28,7 +28,7 @@ LICENSE - + diff --git a/Common/QuantConnect.csproj b/Common/QuantConnect.csproj index 9ba34d1e87bd..9fa973efd104 100644 --- a/Common/QuantConnect.csproj +++ b/Common/QuantConnect.csproj @@ -35,7 +35,7 @@ - + diff --git a/Engine/QuantConnect.Lean.Engine.csproj b/Engine/QuantConnect.Lean.Engine.csproj index feab03df4c47..6ff44486c48d 100644 --- a/Engine/QuantConnect.Lean.Engine.csproj +++ b/Engine/QuantConnect.Lean.Engine.csproj @@ -41,7 +41,7 @@ - + diff --git a/Indicators/QuantConnect.Indicators.csproj b/Indicators/QuantConnect.Indicators.csproj index 56eda6417efa..62ffe9192c61 100644 --- a/Indicators/QuantConnect.Indicators.csproj +++ b/Indicators/QuantConnect.Indicators.csproj @@ -31,7 +31,7 @@ - + diff --git a/Report/QuantConnect.Report.csproj b/Report/QuantConnect.Report.csproj index 7aa0f4405e3e..c81fc164aa18 100644 --- a/Report/QuantConnect.Report.csproj +++ b/Report/QuantConnect.Report.csproj @@ -39,7 +39,7 @@ LICENSE - + diff --git a/Research/QuantConnect.Research.csproj b/Research/QuantConnect.Research.csproj index 005c480ec416..728c5f33ec22 100644 --- a/Research/QuantConnect.Research.csproj +++ b/Research/QuantConnect.Research.csproj @@ -34,7 +34,7 @@ - + diff --git a/Tests/Python/PythonThreadingTests.cs b/Tests/Python/PythonThreadingTests.cs index 259dab40995f..d91b9256293e 100644 --- a/Tests/Python/PythonThreadingTests.cs +++ b/Tests/Python/PythonThreadingTests.cs @@ -14,10 +14,8 @@ * */ -using System; using Python.Runtime; using NUnit.Framework; -using System.Threading; using QuantConnect.Python; using System.Threading.Tasks; @@ -26,87 +24,6 @@ namespace QuantConnect.Tests.Python [TestFixture] public class PythonThreadingTests { - [TestCase("Field", false)] - [TestCase("Func()", false)] - [TestCase("Property", false)] - [TestCase("Field", true)] - [TestCase("Func()", true)] - [TestCase("Property", true)] - public void CallingCShapReleasesGil(string target, bool useInstance) - { - var lockInstance = new object(); - - using var tookGil = new ManualResetEvent(false); - using var tookLock = new ManualResetEvent(false); - - PyObject method; - PyObject propertyWrapper; - using (Py.GIL()) - { - var module = PyModule.FromString("ReleaseGil", $@" -from clr import AddReference -AddReference('QuantConnect.Tests') - -from QuantConnect.Tests.Python import * -import time - -def Method(): - return 1 - -def PropertyCaller(tookGil, tookLock, useInstance): - tookGil.Set() - tookLock.WaitOne() - if useInstance: - instance = PythonThreadingTests.TestPropertyWrapper() - return instance.Instance{target} - else: - return PythonThreadingTests.TestPropertyWrapper.{target} -"); - method = module.GetAttr("Method"); - propertyWrapper = module.GetAttr("PropertyCaller"); - } - - TestPropertyWrapper.Func = () => - { - lock (lockInstance) - { - Thread.Sleep(500); - using (Py.GIL()) - { - return method.Invoke().As(); - } - } - }; - - // task1: has the GIL, go into python, go back into C# and want the C# lock, so he should release the GIL - // task2: has the C# lock and want's the GIL - var task1 = Task.Run(() => - { - using (Py.GIL()) - { - propertyWrapper.Invoke(tookGil, tookLock, useInstance); - } - }); - - var task2 = Task.Run(() => - { - lock (lockInstance) - { - // we take the C# lock and wait and try to get the py gil which should be taken by task 1 - tookLock.Set(); - tookGil.WaitOne(); - using (Py.GIL()) - { - method.Invoke(); - } - } - }); - - var result = Task.WaitAll(new[] { task1, task2 }, TimeSpan.FromSeconds(3)); - - Assert.IsTrue(result); - } - [Test] public void ImportsCanBeExecutedFromDifferentThreads() { @@ -131,21 +48,5 @@ public void ImportsCanBeExecutedFromDifferentThreads() } }).Wait(); } - public class TestPropertyWrapper - { - public static Func Func { get; set; } - public static int Field => Func(); - public static int Property - { - get - { - return Func(); - } - } - - public Func InstanceFunc => Func; - public int InstanceField => Field; - public int InstanceProperty => Property; - } } } diff --git a/Tests/QuantConnect.Tests.csproj b/Tests/QuantConnect.Tests.csproj index 9a8a899176c6..87810318cc75 100644 --- a/Tests/QuantConnect.Tests.csproj +++ b/Tests/QuantConnect.Tests.csproj @@ -31,7 +31,7 @@ - +