From 80b06479046b69edb94045967baee82c20a2d18e Mon Sep 17 00:00:00 2001 From: Josue Nina Date: Mon, 20 Jan 2025 16:32:10 -0500 Subject: [PATCH 1/9] Create regression tests and refactor liquidation logic - Implemented regression tests for order creation and liquidation scenarios - Removed LiquidateExistingHoldings method - Replaced LiquidateExistingHoldings with the general Liquidate method --- .../LiquidateRegressionAlgorithm.cs | 128 ++++++++++++++++++ ...dateUsingSetHoldingsRegressionAlgorithm.cs | 16 +++ Algorithm/QCAlgorithm.Trading.cs | 37 ++--- 3 files changed, 152 insertions(+), 29 deletions(-) create mode 100644 Algorithm.CSharp/LiquidateRegressionAlgorithm.cs create mode 100644 Algorithm.CSharp/RegressionTests/LiquidateUsingSetHoldingsRegressionAlgorithm.cs diff --git a/Algorithm.CSharp/LiquidateRegressionAlgorithm.cs b/Algorithm.CSharp/LiquidateRegressionAlgorithm.cs new file mode 100644 index 000000000000..aa90876e4b08 --- /dev/null +++ b/Algorithm.CSharp/LiquidateRegressionAlgorithm.cs @@ -0,0 +1,128 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using QuantConnect.Algorithm.Framework.Portfolio; +using QuantConnect.Data; +using QuantConnect.Interfaces; +using QuantConnect.Orders; + +namespace QuantConnect.Algorithm.CSharp +{ + public class LiquidateRegressionAlgorithm : QCAlgorithm, IRegressionAlgorithmDefinition + { + protected Symbol _spy; + protected Symbol _ibm; + public override void Initialize() + { + SetStartDate(2018, 1, 5); + SetEndDate(2018, 1, 10); + SetCash(100000); + _spy = AddEquity("SPY", Resolution.Daily).Symbol; + _ibm = Symbol("IBM R735QTJ8XC9X"); + var security = AddSecurity(_ibm, Resolution.Daily); + + // Schedule Rebalance method to be called on specific dates + Schedule.On(DateRules.On(2018, 1, 5), TimeRules.Midnight, Rebalance); + Schedule.On(DateRules.On(2018, 1, 8), TimeRules.Midnight, Rebalance); + } + + public virtual void Rebalance() + { + // Place a MarketOrder + MarketOrder(_ibm, 10); + + // Place a LimitOrder to sell 1 share at a price below the current market price + LimitOrder(_ibm, 1, Securities[_ibm].Price - 5); + LimitOrder(_spy, 1, Securities[_spy].Price - 5); + + // Liquidate all holdings immediately + PerformLiquidation(); + } + + public virtual void PerformLiquidation() + { + Liquidate(); + } + + public override void OnEndOfAlgorithm() + { + // Check if there are any orders that should have been canceled + var orders = Transactions.GetOrders().ToList(); + var cnt = orders.Where(e => e.Status != OrderStatus.Canceled).Count(); + if (cnt > 0) + { + throw new RegressionTestException($"There are {cnt} orders that should have been cancelled"); + } + + // Check if there are any holdings left in the portfolio + foreach (var kvp in Portfolio) + { + var symbol = kvp.Key; + var holdings = kvp.Value; + if (holdings.Quantity != 0) + { + throw new RegressionTestException("There are holdings in portfolio"); + } + } + } + + /// + /// Final status of the algorithm + /// + public AlgorithmStatus AlgorithmStatus => AlgorithmStatus.Completed; + + /// + /// This is used by the regression test system to indicate if the open source Lean repository has the required data to run this algorithm. + /// + public bool CanRunLocally { get; } = true; + + /// + /// This is used by the regression test system to indicate which languages this algorithm is written in. + /// + public List Languages { get; } = new() { Language.CSharp }; + + /// + /// Data Points count of all timeslices of algorithm + /// + public long DataPoints => 44; + + /// + /// Data Points count of the algorithm history + /// + public int AlgorithmHistoryDataPoints => 0; + + /// + /// This is used by the regression test system to indicate what the expected statistics are from running the algorithm + /// + public Dictionary ExpectedStatistics => new Dictionary + { + {"Total Orders", "3"}, + {"Average Win", "0%"}, + {"Average Loss", "0%"}, + {"Compounding Annual Return", "0%"}, + {"Drawdown", "0%"}, + {"Expectancy", "0"}, + {"Start Equity", "100000"}, + {"End Equity", "100000"}, + {"Net Profit", "0%"}, + {"Sharpe Ratio", "0"}, + {"Sortino Ratio", "0"}, + {"Probabilistic Sharpe Ratio", "0%"}, + {"Loss Rate", "0%"}, + {"Win Rate", "0%"}, + {"Profit-Loss Ratio", "0"}, + {"Alpha", "0"}, + {"Beta", "0"}, + {"Annual Standard Deviation", "0"}, + {"Annual Variance", "0"}, + {"Information Ratio", "-5.634"}, + {"Tracking Error", "0.024"}, + {"Treynor Ratio", "0"}, + {"Total Fees", "$0.00"}, + {"Estimated Strategy Capacity", "$0"}, + {"Lowest Capacity Asset", ""}, + {"Portfolio Turnover", "0%"}, + {"OrderListHash", "3dc667d309559a7df141959a22aef64c"} + }; + } +} diff --git a/Algorithm.CSharp/RegressionTests/LiquidateUsingSetHoldingsRegressionAlgorithm.cs b/Algorithm.CSharp/RegressionTests/LiquidateUsingSetHoldingsRegressionAlgorithm.cs new file mode 100644 index 000000000000..0e52d6ab6ff8 --- /dev/null +++ b/Algorithm.CSharp/RegressionTests/LiquidateUsingSetHoldingsRegressionAlgorithm.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using QuantConnect.Algorithm.Framework.Portfolio; + +namespace QuantConnect.Algorithm.CSharp.RegressionTests +{ + public class LiquidateUsingSetHoldingsRegressionAlgorithm : LiquidateRegressionAlgorithm + { + public override void PerformLiquidation() + { + SetHoldings(new List(), true); + } + } +} diff --git a/Algorithm/QCAlgorithm.Trading.cs b/Algorithm/QCAlgorithm.Trading.cs index 1ef0ba2c53e5..86bc1c65f3a8 100644 --- a/Algorithm/QCAlgorithm.Trading.cs +++ b/Algorithm/QCAlgorithm.Trading.cs @@ -712,7 +712,7 @@ public OrderTicket LimitIfTouchedOrder(Symbol symbol, decimal quantity, decimal [DocumentationAttribute(TradingAndOrders)] public OrderTicket ExerciseOption(Symbol optionSymbol, int quantity, bool asynchronous = false, string tag = "", IOrderProperties orderProperties = null) { - var option = (Option) Securities[optionSymbol]; + var option = (Option)Securities[optionSymbol]; // SubmitOrderRequest.Quantity indicates the change in holdings quantity, therefore manual exercise quantities must be negative // PreOrderChecksImpl confirms that we don't hold a short position, so we're lenient here and accept +/- quantity values @@ -873,7 +873,7 @@ private IEnumerable GenerateOptionStrategyOrders(OptionStrategy str } } - if(leg == null) + if (leg == null) { throw new InvalidOperationException("Couldn't find the option contract in algorithm securities list. " + Invariant($"Underlying: {strategy.Underlying}, option {optionLeg.Right}, strike {optionLeg.Strike}, ") + @@ -891,7 +891,7 @@ private List SubmitComboOrder(List legs, decimal quantity, dec CheckComboOrderSizing(legs, quantity); var orderType = OrderType.ComboMarket; - if(limitPrice != 0) + if (limitPrice != 0) { orderType = OrderType.ComboLimit; } @@ -1237,7 +1237,7 @@ public List Liquidate(IEnumerable symbols, bool asynchronou Debug("Liquidate() is currently disabled by settings. To re-enable please set 'Settings.LiquidateEnabled' to true"); return orderTickets; } - + foreach (var symbolToLiquidate in symbols) { // get open orders @@ -1300,7 +1300,7 @@ public List Liquidate(IEnumerable symbols, bool asynchronou [Obsolete($"This method is obsolete, please use Liquidate(symbol: symbolToLiquidate, tag: tag) method")] public List Liquidate(Symbol symbolToLiquidate, string tag) { - return Liquidate(symbol: symbolToLiquidate, tag:tag).Select(x => x.OrderId).ToList(); + return Liquidate(symbol: symbolToLiquidate, tag: tag).Select(x => x.OrderId).ToList(); } /// @@ -1332,13 +1332,13 @@ public void SetHoldings(List targets, bool liquidateExistingHol //If they triggered a liquidate if (liquidateExistingHoldings) { - LiquidateExistingHoldings(targets.Select(x => x.Symbol).ToHashSet(), tag, orderProperties); + Liquidate(); } foreach (var portfolioTarget in targets // we need to create targets with quantities for OrderTargetsByMarginImpact .Select(target => new PortfolioTarget(target.Symbol, CalculateOrderQuantity(target.Symbol, target.Quantity))) - .OrderTargetsByMarginImpact(this, targetIsDelta:true)) + .OrderTargetsByMarginImpact(this, targetIsDelta: true)) { SetHoldingsImpl(portfolioTarget.Symbol, portfolioTarget.Quantity, false, tag, orderProperties); } @@ -1415,7 +1415,7 @@ private void SetHoldingsImpl(Symbol symbol, decimal orderQuantity, bool liquidat //If they triggered a liquidate if (liquidateExistingHoldings) { - LiquidateExistingHoldings(new HashSet { symbol }, tag, orderProperties); + Liquidate(); } //Calculate total unfilled quantity for open market orders @@ -1448,27 +1448,6 @@ private void SetHoldingsImpl(Symbol symbol, decimal orderQuantity, bool liquidat } } - /// - /// Liquidate existing holdings, except for the target list of Symbol. - /// - /// List of Symbol indexer - /// Tag the order with a short string. - /// The order properties to use. Defaults to - private void LiquidateExistingHoldings(HashSet symbols, string tag = "", IOrderProperties orderProperties = null) - { - foreach (var kvp in Portfolio) - { - var holdingSymbol = kvp.Key; - var holdings = kvp.Value; - if (!symbols.Contains(holdingSymbol) && holdings.AbsoluteQuantity > 0) - { - //Go through all existing holdings [synchronously], market order the inverse quantity: - var liquidationQuantity = CalculateOrderQuantity(holdingSymbol, 0m); - Order(holdingSymbol, liquidationQuantity, false, tag, orderProperties); - } - } - } - /// /// Calculate the order quantity to achieve target-percent holdings. /// From 9bbba46a90ed557e8b7b572f4b490001e06da2d9 Mon Sep 17 00:00:00 2001 From: Josue Nina Date: Tue, 21 Jan 2025 09:59:25 -0500 Subject: [PATCH 2/9] Addressed review comments --- .../LiquidateRegressionAlgorithm.cs | 59 ++++++++++++++----- ...dateUsingSetHoldingsRegressionAlgorithm.cs | 21 ++++++- Algorithm/QCAlgorithm.Trading.cs | 2 +- 3 files changed, 64 insertions(+), 18 deletions(-) diff --git a/Algorithm.CSharp/LiquidateRegressionAlgorithm.cs b/Algorithm.CSharp/LiquidateRegressionAlgorithm.cs index aa90876e4b08..67ad19ac0e65 100644 --- a/Algorithm.CSharp/LiquidateRegressionAlgorithm.cs +++ b/Algorithm.CSharp/LiquidateRegressionAlgorithm.cs @@ -1,25 +1,42 @@ +/* + * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. + * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + using System; using System.Collections.Generic; using System.Linq; -using QuantConnect.Algorithm.Framework.Portfolio; -using QuantConnect.Data; using QuantConnect.Interfaces; using QuantConnect.Orders; namespace QuantConnect.Algorithm.CSharp { + /// + /// A regression test algorithm that places market and limit orders, then liquidates all holdings, + /// ensuring orders are canceled and the portfolio is empty. + /// public class LiquidateRegressionAlgorithm : QCAlgorithm, IRegressionAlgorithmDefinition { protected Symbol _spy; protected Symbol _ibm; public override void Initialize() { - SetStartDate(2018, 1, 5); + SetStartDate(2018, 1, 4); SetEndDate(2018, 1, 10); SetCash(100000); _spy = AddEquity("SPY", Resolution.Daily).Symbol; _ibm = Symbol("IBM R735QTJ8XC9X"); - var security = AddSecurity(_ibm, Resolution.Daily); + AddSecurity(_ibm, Resolution.Daily); // Schedule Rebalance method to be called on specific dates Schedule.On(DateRules.On(2018, 1, 5), TimeRules.Midnight, Rebalance); @@ -33,9 +50,23 @@ public virtual void Rebalance() // Place a LimitOrder to sell 1 share at a price below the current market price LimitOrder(_ibm, 1, Securities[_ibm].Price - 5); + LimitOrder(_spy, 1, Securities[_spy].Price - 5); + var spyOrder = Transactions.GetOpenOrders(_spy).FirstOrDefault(); + // Ensure there is an open order for SPY + if (spyOrder == null) + { + throw new RegressionTestException("There should be an open order for SPY."); + } + // Liquidate SPY orders and verify cancellation + SetHoldings(_spy, 1, true); + var spyCancelOrder = Transactions.GetOrderById(spyOrder.Id); + if (spyCancelOrder.Status != OrderStatus.Canceled) + { + throw new RegressionTestException("The SPY order should be cancelled."); + } - // Liquidate all holdings immediately + // Liquidate all remaining holdings immediately PerformLiquidation(); } @@ -48,10 +79,10 @@ public override void OnEndOfAlgorithm() { // Check if there are any orders that should have been canceled var orders = Transactions.GetOrders().ToList(); - var cnt = orders.Where(e => e.Status != OrderStatus.Canceled).Count(); - if (cnt > 0) + var nonCanceledOrdersCount = orders.Where(e => e.Status != OrderStatus.Canceled).Count(); + if (nonCanceledOrdersCount > 0) { - throw new RegressionTestException($"There are {cnt} orders that should have been cancelled"); + throw new RegressionTestException($"There are {nonCanceledOrdersCount} orders that should have been cancelled"); } // Check if there are any holdings left in the portfolio @@ -61,7 +92,7 @@ public override void OnEndOfAlgorithm() var holdings = kvp.Value; if (holdings.Quantity != 0) { - throw new RegressionTestException("There are holdings in portfolio"); + throw new RegressionTestException($"There are {holdings.Quantity} holdings of {symbol} in the portfolio"); } } } @@ -84,7 +115,7 @@ public override void OnEndOfAlgorithm() /// /// Data Points count of all timeslices of algorithm /// - public long DataPoints => 44; + public long DataPoints => 53; /// /// Data Points count of the algorithm history @@ -96,7 +127,7 @@ public override void OnEndOfAlgorithm() /// public Dictionary ExpectedStatistics => new Dictionary { - {"Total Orders", "3"}, + {"Total Orders", "8"}, {"Average Win", "0%"}, {"Average Loss", "0%"}, {"Compounding Annual Return", "0%"}, @@ -115,14 +146,14 @@ public override void OnEndOfAlgorithm() {"Beta", "0"}, {"Annual Standard Deviation", "0"}, {"Annual Variance", "0"}, - {"Information Ratio", "-5.634"}, - {"Tracking Error", "0.024"}, + {"Information Ratio", "-10.398"}, + {"Tracking Error", "0.045"}, {"Treynor Ratio", "0"}, {"Total Fees", "$0.00"}, {"Estimated Strategy Capacity", "$0"}, {"Lowest Capacity Asset", ""}, {"Portfolio Turnover", "0%"}, - {"OrderListHash", "3dc667d309559a7df141959a22aef64c"} + {"OrderListHash", "f03dec5648d761ca660e0ea51403aa92"} }; } } diff --git a/Algorithm.CSharp/RegressionTests/LiquidateUsingSetHoldingsRegressionAlgorithm.cs b/Algorithm.CSharp/RegressionTests/LiquidateUsingSetHoldingsRegressionAlgorithm.cs index 0e52d6ab6ff8..691c7baef2cf 100644 --- a/Algorithm.CSharp/RegressionTests/LiquidateUsingSetHoldingsRegressionAlgorithm.cs +++ b/Algorithm.CSharp/RegressionTests/LiquidateUsingSetHoldingsRegressionAlgorithm.cs @@ -1,11 +1,26 @@ -using System; +/* + * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. + * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using QuantConnect.Algorithm.Framework.Portfolio; namespace QuantConnect.Algorithm.CSharp.RegressionTests { + /// + /// A regression test algorithm that uses SetHoldings to liquidate the portfolio by setting holdings to zero. + /// public class LiquidateUsingSetHoldingsRegressionAlgorithm : LiquidateRegressionAlgorithm { public override void PerformLiquidation() diff --git a/Algorithm/QCAlgorithm.Trading.cs b/Algorithm/QCAlgorithm.Trading.cs index 86bc1c65f3a8..409282a81398 100644 --- a/Algorithm/QCAlgorithm.Trading.cs +++ b/Algorithm/QCAlgorithm.Trading.cs @@ -1415,7 +1415,7 @@ private void SetHoldingsImpl(Symbol symbol, decimal orderQuantity, bool liquidat //If they triggered a liquidate if (liquidateExistingHoldings) { - Liquidate(); + Liquidate(symbol); } //Calculate total unfilled quantity for open market orders From 2fc8f99e29e584db9e45e04098d4e2715d045bcb Mon Sep 17 00:00:00 2001 From: Josue Nina Date: Tue, 21 Jan 2025 11:43:52 -0500 Subject: [PATCH 3/9] Addressed new comments review --- .../LiquidateRegressionAlgorithm.cs | 15 ++++-- ...dateUsingSetHoldingsRegressionAlgorithm.cs | 47 ++++++++++++++++++- Algorithm/QCAlgorithm.Trading.cs | 6 ++- 3 files changed, 62 insertions(+), 6 deletions(-) diff --git a/Algorithm.CSharp/LiquidateRegressionAlgorithm.cs b/Algorithm.CSharp/LiquidateRegressionAlgorithm.cs index 67ad19ac0e65..0ef7ac7f6acb 100644 --- a/Algorithm.CSharp/LiquidateRegressionAlgorithm.cs +++ b/Algorithm.CSharp/LiquidateRegressionAlgorithm.cs @@ -59,12 +59,21 @@ public virtual void Rebalance() throw new RegressionTestException("There should be an open order for SPY."); } // Liquidate SPY orders and verify cancellation - SetHoldings(_spy, 1, true); + var orderProperties = new OrderProperties { TimeInForce = TimeInForce.GoodTilCanceled }; + SetHoldings(_spy, 1, true, "LiquidatedTest", orderProperties); var spyCancelOrder = Transactions.GetOrderById(spyOrder.Id); if (spyCancelOrder.Status != OrderStatus.Canceled) { throw new RegressionTestException("The SPY order should be cancelled."); } + if (spyCancelOrder.Tag != "LiquidatedTest") + { + throw new RegressionTestException("The SPY order should have the tag LiquidatedTest."); + } + if (spyCancelOrder.Properties.TimeInForce != TimeInForce.GoodTilCanceled) + { + throw new RegressionTestException("The SPY order should have the TimeInForce set to GoodTilCanceled."); + } // Liquidate all remaining holdings immediately PerformLiquidation(); @@ -125,7 +134,7 @@ public override void OnEndOfAlgorithm() /// /// This is used by the regression test system to indicate what the expected statistics are from running the algorithm /// - public Dictionary ExpectedStatistics => new Dictionary + public virtual Dictionary ExpectedStatistics => new Dictionary { {"Total Orders", "8"}, {"Average Win", "0%"}, @@ -153,7 +162,7 @@ public override void OnEndOfAlgorithm() {"Estimated Strategy Capacity", "$0"}, {"Lowest Capacity Asset", ""}, {"Portfolio Turnover", "0%"}, - {"OrderListHash", "f03dec5648d761ca660e0ea51403aa92"} + {"OrderListHash", "8d47e98571918500e95df416bfe21fdf"} }; } } diff --git a/Algorithm.CSharp/RegressionTests/LiquidateUsingSetHoldingsRegressionAlgorithm.cs b/Algorithm.CSharp/RegressionTests/LiquidateUsingSetHoldingsRegressionAlgorithm.cs index 691c7baef2cf..5f60bbf7492f 100644 --- a/Algorithm.CSharp/RegressionTests/LiquidateUsingSetHoldingsRegressionAlgorithm.cs +++ b/Algorithm.CSharp/RegressionTests/LiquidateUsingSetHoldingsRegressionAlgorithm.cs @@ -14,7 +14,9 @@ */ using System.Collections.Generic; +using System.Linq; using QuantConnect.Algorithm.Framework.Portfolio; +using QuantConnect.Orders; namespace QuantConnect.Algorithm.CSharp.RegressionTests { @@ -25,7 +27,50 @@ public class LiquidateUsingSetHoldingsRegressionAlgorithm : LiquidateRegressionA { public override void PerformLiquidation() { - SetHoldings(new List(), true); + var properties = new OrderProperties { TimeInForce = TimeInForce.GoodTilCanceled }; + SetHoldings(new List(), true, "LiquidatedTest", properties); + var orders = Transactions.GetOrders().ToList(); + var orderTags = orders.Where(e => e.Tag == "LiquidatedTest").ToList(); + if (orderTags.Count != orders.Count) + { + throw new RegressionTestException("The tag was not set on all orders"); + } + var orderProperties = orders.Where(e => e.Properties.TimeInForce == TimeInForce.GoodTilCanceled).ToList(); + if (orderProperties.Count != orders.Count) + { + throw new RegressionTestException("The properties were not set on all orders"); + } } + + public override Dictionary ExpectedStatistics => new Dictionary + { + {"Total Orders", "8"}, + {"Average Win", "0%"}, + {"Average Loss", "0%"}, + {"Compounding Annual Return", "0%"}, + {"Drawdown", "0%"}, + {"Expectancy", "0"}, + {"Start Equity", "100000"}, + {"End Equity", "100000"}, + {"Net Profit", "0%"}, + {"Sharpe Ratio", "0"}, + {"Sortino Ratio", "0"}, + {"Probabilistic Sharpe Ratio", "0%"}, + {"Loss Rate", "0%"}, + {"Win Rate", "0%"}, + {"Profit-Loss Ratio", "0"}, + {"Alpha", "0"}, + {"Beta", "0"}, + {"Annual Standard Deviation", "0"}, + {"Annual Variance", "0"}, + {"Information Ratio", "-10.398"}, + {"Tracking Error", "0.045"}, + {"Treynor Ratio", "0"}, + {"Total Fees", "$0.00"}, + {"Estimated Strategy Capacity", "$0"}, + {"Lowest Capacity Asset", ""}, + {"Portfolio Turnover", "0%"}, + {"OrderListHash", "25c4614305b6849ddec931fdf453af55"} + }; } } diff --git a/Algorithm/QCAlgorithm.Trading.cs b/Algorithm/QCAlgorithm.Trading.cs index 409282a81398..d197f011a1bb 100644 --- a/Algorithm/QCAlgorithm.Trading.cs +++ b/Algorithm/QCAlgorithm.Trading.cs @@ -1332,7 +1332,8 @@ public void SetHoldings(List targets, bool liquidateExistingHol //If they triggered a liquidate if (liquidateExistingHoldings) { - Liquidate(); + var liquidateTag = string.IsNullOrWhiteSpace(tag) ? "Liquidated" : tag; + Liquidate(tag: liquidateTag, orderProperties: orderProperties); } foreach (var portfolioTarget in targets @@ -1415,7 +1416,8 @@ private void SetHoldingsImpl(Symbol symbol, decimal orderQuantity, bool liquidat //If they triggered a liquidate if (liquidateExistingHoldings) { - Liquidate(symbol); + var liquidateTag = string.IsNullOrWhiteSpace(tag) ? "Liquidated" : tag; + Liquidate(symbol, tag: liquidateTag, orderProperties: orderProperties); } //Calculate total unfilled quantity for open market orders From 337003e7366aaa71e0561643abbfdd8111693da8 Mon Sep 17 00:00:00 2001 From: Josue Nina Date: Tue, 21 Jan 2025 12:10:31 -0500 Subject: [PATCH 4/9] Update default value for 'tag' --- Algorithm.CSharp/LiquidateRegressionAlgorithm.cs | 3 +-- Algorithm/QCAlgorithm.Trading.cs | 15 +++++++-------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/Algorithm.CSharp/LiquidateRegressionAlgorithm.cs b/Algorithm.CSharp/LiquidateRegressionAlgorithm.cs index 0ef7ac7f6acb..fcbbe42aec49 100644 --- a/Algorithm.CSharp/LiquidateRegressionAlgorithm.cs +++ b/Algorithm.CSharp/LiquidateRegressionAlgorithm.cs @@ -35,8 +35,7 @@ public override void Initialize() SetEndDate(2018, 1, 10); SetCash(100000); _spy = AddEquity("SPY", Resolution.Daily).Symbol; - _ibm = Symbol("IBM R735QTJ8XC9X"); - AddSecurity(_ibm, Resolution.Daily); + _ibm = AddEquity("IBM", Resolution.Daily).Symbol; // Schedule Rebalance method to be called on specific dates Schedule.On(DateRules.On(2018, 1, 5), TimeRules.Midnight, Rebalance); diff --git a/Algorithm/QCAlgorithm.Trading.cs b/Algorithm/QCAlgorithm.Trading.cs index d197f011a1bb..3c8b9494cc2e 100644 --- a/Algorithm/QCAlgorithm.Trading.cs +++ b/Algorithm/QCAlgorithm.Trading.cs @@ -1205,7 +1205,7 @@ private OrderResponse PreOrderChecksImpl(SubmitOrderRequest request) /// Custom tag to know who is calling this /// Order properties to use [DocumentationAttribute(TradingAndOrders)] - public List Liquidate(Symbol symbol = null, bool asynchronous = false, string tag = "Liquidated", IOrderProperties orderProperties = null) + public List Liquidate(Symbol symbol = null, bool asynchronous = false, string tag = null, IOrderProperties orderProperties = null) { IEnumerable toLiquidate; if (symbol != null) @@ -1229,7 +1229,7 @@ public List Liquidate(Symbol symbol = null, bool asynchronous = fal /// Custom tag to know who is calling this /// Order properties to use [DocumentationAttribute(TradingAndOrders)] - public List Liquidate(IEnumerable symbols, bool asynchronous = false, string tag = "Liquidated", IOrderProperties orderProperties = null) + public List Liquidate(IEnumerable symbols, bool asynchronous = false, string tag = null, IOrderProperties orderProperties = null) { var orderTickets = new List(); if (!Settings.LiquidateEnabled) @@ -1238,6 +1238,7 @@ public List Liquidate(IEnumerable symbols, bool asynchronou return orderTickets; } + tag ??= "Liquidated"; foreach (var symbolToLiquidate in symbols) { // get open orders @@ -1327,13 +1328,12 @@ public void SetMaximumOrders(int max) /// The order properties to use. Defaults to /// [DocumentationAttribute(TradingAndOrders)] - public void SetHoldings(List targets, bool liquidateExistingHoldings = false, string tag = "", IOrderProperties orderProperties = null) + public void SetHoldings(List targets, bool liquidateExistingHoldings = false, string tag = null, IOrderProperties orderProperties = null) { //If they triggered a liquidate if (liquidateExistingHoldings) { - var liquidateTag = string.IsNullOrWhiteSpace(tag) ? "Liquidated" : tag; - Liquidate(tag: liquidateTag, orderProperties: orderProperties); + Liquidate(tag: tag, orderProperties: orderProperties); } foreach (var portfolioTarget in targets @@ -1411,13 +1411,12 @@ public void SetHoldings(Symbol symbol, decimal percentage, bool liquidateExistin /// /// Set holdings implementation, which uses order quantities (delta) not percentage nor target final quantity /// - private void SetHoldingsImpl(Symbol symbol, decimal orderQuantity, bool liquidateExistingHoldings = false, string tag = "", IOrderProperties orderProperties = null) + private void SetHoldingsImpl(Symbol symbol, decimal orderQuantity, bool liquidateExistingHoldings = false, string tag = null, IOrderProperties orderProperties = null) { //If they triggered a liquidate if (liquidateExistingHoldings) { - var liquidateTag = string.IsNullOrWhiteSpace(tag) ? "Liquidated" : tag; - Liquidate(symbol, tag: liquidateTag, orderProperties: orderProperties); + Liquidate(symbol, tag: tag, orderProperties: orderProperties); } //Calculate total unfilled quantity for open market orders From d7c9a9114d58e1e2a385c19fff403898d00968e2 Mon Sep 17 00:00:00 2001 From: Josue Nina Date: Tue, 21 Jan 2025 14:07:58 -0500 Subject: [PATCH 5/9] Update ExpectedStatistics --- Algorithm.CSharp/VolumeShareSlippageModelAlgorithm.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Algorithm.CSharp/VolumeShareSlippageModelAlgorithm.cs b/Algorithm.CSharp/VolumeShareSlippageModelAlgorithm.cs index 5f6861c6b935..14115ccf2209 100644 --- a/Algorithm.CSharp/VolumeShareSlippageModelAlgorithm.cs +++ b/Algorithm.CSharp/VolumeShareSlippageModelAlgorithm.cs @@ -103,7 +103,7 @@ public override void OnData(Slice slice) /// public Dictionary ExpectedStatistics => new Dictionary { - {"Total Orders", "4"}, + {"Total Orders", "8"}, {"Average Win", "0%"}, {"Average Loss", "0%"}, {"Compounding Annual Return", "20.900%"}, @@ -129,7 +129,7 @@ public override void OnData(Slice slice) {"Estimated Strategy Capacity", "$4400000000.00"}, {"Lowest Capacity Asset", "GOOCV VP83T1ZUHROL"}, {"Portfolio Turnover", "4.22%"}, - {"OrderListHash", "9d2bd0df7c094c393e77f72b7739bfa0"} + {"OrderListHash", "ea6ee0b29e9280f82d468288a8bc2e75"} }; } } From 0b093fb537f2cb0ce18c87978620dff9733c44da Mon Sep 17 00:00:00 2001 From: Josue Nina Date: Tue, 21 Jan 2025 14:51:43 -0500 Subject: [PATCH 6/9] Identify and liquidate portfolio symbols not included in targets --- Algorithm.CSharp/VolumeShareSlippageModelAlgorithm.cs | 4 ++-- Algorithm/QCAlgorithm.Trading.cs | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Algorithm.CSharp/VolumeShareSlippageModelAlgorithm.cs b/Algorithm.CSharp/VolumeShareSlippageModelAlgorithm.cs index 14115ccf2209..5f6861c6b935 100644 --- a/Algorithm.CSharp/VolumeShareSlippageModelAlgorithm.cs +++ b/Algorithm.CSharp/VolumeShareSlippageModelAlgorithm.cs @@ -103,7 +103,7 @@ public override void OnData(Slice slice) /// public Dictionary ExpectedStatistics => new Dictionary { - {"Total Orders", "8"}, + {"Total Orders", "4"}, {"Average Win", "0%"}, {"Average Loss", "0%"}, {"Compounding Annual Return", "20.900%"}, @@ -129,7 +129,7 @@ public override void OnData(Slice slice) {"Estimated Strategy Capacity", "$4400000000.00"}, {"Lowest Capacity Asset", "GOOCV VP83T1ZUHROL"}, {"Portfolio Turnover", "4.22%"}, - {"OrderListHash", "ea6ee0b29e9280f82d468288a8bc2e75"} + {"OrderListHash", "9d2bd0df7c094c393e77f72b7739bfa0"} }; } } diff --git a/Algorithm/QCAlgorithm.Trading.cs b/Algorithm/QCAlgorithm.Trading.cs index 3c8b9494cc2e..866f23af59aa 100644 --- a/Algorithm/QCAlgorithm.Trading.cs +++ b/Algorithm/QCAlgorithm.Trading.cs @@ -1333,7 +1333,11 @@ public void SetHoldings(List targets, bool liquidateExistingHol //If they triggered a liquidate if (liquidateExistingHoldings) { - Liquidate(tag: tag, orderProperties: orderProperties); + var targetSymbols = new HashSet(targets.Select(t => t.Symbol)); + var symbolsToLiquidate = Portfolio.Keys + .Where(symbol => !targetSymbols.Contains(symbol)) + .ToList(); + Liquidate(symbolsToLiquidate, tag: tag, orderProperties: orderProperties); } foreach (var portfolioTarget in targets From f76f48917fb3be1aa855731ea052b9e54198203a Mon Sep 17 00:00:00 2001 From: Josue Nina Date: Tue, 21 Jan 2025 17:32:09 -0500 Subject: [PATCH 7/9] Create a new regression test --- .../LiquidateRegressionAlgorithm.cs | 26 +---- ...xceptSpecifiedSymbolRegressionAlgorithm.cs | 107 ++++++++++++++++++ ...dateUsingSetHoldingsRegressionAlgorithm.cs | 4 +- Algorithm/QCAlgorithm.Trading.cs | 34 ++++-- 4 files changed, 133 insertions(+), 38 deletions(-) create mode 100644 Algorithm.CSharp/RegressionTests/LiquidateAllExceptSpecifiedSymbolRegressionAlgorithm.cs diff --git a/Algorithm.CSharp/LiquidateRegressionAlgorithm.cs b/Algorithm.CSharp/LiquidateRegressionAlgorithm.cs index fcbbe42aec49..d6176fa06a1f 100644 --- a/Algorithm.CSharp/LiquidateRegressionAlgorithm.cs +++ b/Algorithm.CSharp/LiquidateRegressionAlgorithm.cs @@ -51,28 +51,6 @@ public virtual void Rebalance() LimitOrder(_ibm, 1, Securities[_ibm].Price - 5); LimitOrder(_spy, 1, Securities[_spy].Price - 5); - var spyOrder = Transactions.GetOpenOrders(_spy).FirstOrDefault(); - // Ensure there is an open order for SPY - if (spyOrder == null) - { - throw new RegressionTestException("There should be an open order for SPY."); - } - // Liquidate SPY orders and verify cancellation - var orderProperties = new OrderProperties { TimeInForce = TimeInForce.GoodTilCanceled }; - SetHoldings(_spy, 1, true, "LiquidatedTest", orderProperties); - var spyCancelOrder = Transactions.GetOrderById(spyOrder.Id); - if (spyCancelOrder.Status != OrderStatus.Canceled) - { - throw new RegressionTestException("The SPY order should be cancelled."); - } - if (spyCancelOrder.Tag != "LiquidatedTest") - { - throw new RegressionTestException("The SPY order should have the tag LiquidatedTest."); - } - if (spyCancelOrder.Properties.TimeInForce != TimeInForce.GoodTilCanceled) - { - throw new RegressionTestException("The SPY order should have the TimeInForce set to GoodTilCanceled."); - } // Liquidate all remaining holdings immediately PerformLiquidation(); @@ -135,7 +113,7 @@ public override void OnEndOfAlgorithm() /// public virtual Dictionary ExpectedStatistics => new Dictionary { - {"Total Orders", "8"}, + {"Total Orders", "6"}, {"Average Win", "0%"}, {"Average Loss", "0%"}, {"Compounding Annual Return", "0%"}, @@ -161,7 +139,7 @@ public override void OnEndOfAlgorithm() {"Estimated Strategy Capacity", "$0"}, {"Lowest Capacity Asset", ""}, {"Portfolio Turnover", "0%"}, - {"OrderListHash", "8d47e98571918500e95df416bfe21fdf"} + {"OrderListHash", "9423c872a626fb856b7c377686c28d85"} }; } } diff --git a/Algorithm.CSharp/RegressionTests/LiquidateAllExceptSpecifiedSymbolRegressionAlgorithm.cs b/Algorithm.CSharp/RegressionTests/LiquidateAllExceptSpecifiedSymbolRegressionAlgorithm.cs new file mode 100644 index 000000000000..567dd311a2f1 --- /dev/null +++ b/Algorithm.CSharp/RegressionTests/LiquidateAllExceptSpecifiedSymbolRegressionAlgorithm.cs @@ -0,0 +1,107 @@ +/* + * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. + * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +using System; +using System.Collections.Generic; +using System.Linq; +using QuantConnect.Orders; + +namespace QuantConnect.Algorithm.CSharp.RegressionTests +{ + /// + /// Tests liquidating all portfolio holdings except a specific symbol, verifying canceled orders and correct tags. + /// + public class LiquidateAllExceptSpecifiedSymbolRegressionAlgorithm : LiquidateRegressionAlgorithm + { + public override void Rebalance() + { + // Place a MarketOrder + MarketOrder(_ibm, 10); + + // Place a LimitOrder to sell 1 share at a price below the current market price + LimitOrder(_ibm, 1, Securities[_ibm].Price - 5); + + // Liquidate SPY orders and verify cancellation + var orderProperties = new OrderProperties { TimeInForce = TimeInForce.GoodTilCanceled }; + SetHoldings(_spy, 1, true, "LiquidatedTest", orderProperties); + } + + public override void OnEndOfAlgorithm() + { + // Retrieve all orders from the Transactions for analysis + var orders = Transactions.GetOrders().ToList(); + + // Count orders that were canceled + var canceledOrdersCount = orders.Where(order => order.Status == OrderStatus.Canceled).Count(); + + // Expectation 1: There should be exactly 4 canceled orders. + // This occurs because Rebalance is called twice, and each call to Rebalance + // (e.g., LimitOrder or MarketOrder) that get canceled due to the Liquidate call in SetHoldings. + if (canceledOrdersCount != 4) + { + throw new RegressionTestException($"Expected 4 canceled orders, but found {canceledOrdersCount}."); + } + + // Count orders that were not canceled + var nonCanceledOrdersCount = orders.Where(order => order.Status != OrderStatus.Canceled).Count(); + + // Expectation 2: There should be exactly 1 non-canceled order after the Liquidate call. + // This occurs because all holdings except SPY are liquidated, and a new order is placed for SPY. + if (nonCanceledOrdersCount != 1) + { + throw new RegressionTestException($"Expected 1 non-canceled order, but found {nonCanceledOrdersCount}."); + } + + // Verify all tags are "LiquidatedTest" + var invalidTags = orders.Where(order => order.Tag != "LiquidatedTest").ToList(); + if (invalidTags.Count != 0) + { + var invalidTagsDetails = string.Join(", ", invalidTags.Select(order => $"OrderID {order.Id}, Tag: {order.Tag}")); + throw new RegressionTestException($"All orders should have the tag 'LiquidatedTest', but found invalid tags: {invalidTagsDetails}."); + } + } + + public override Dictionary ExpectedStatistics => new Dictionary + { + {"Total Orders", "5"}, + {"Average Win", "0%"}, + {"Average Loss", "0%"}, + {"Compounding Annual Return", "36.497%"}, + {"Drawdown", "0.200%"}, + {"Expectancy", "0"}, + {"Start Equity", "100000"}, + {"End Equity", "100569.90"}, + {"Net Profit", "0.570%"}, + {"Sharpe Ratio", "9.031"}, + {"Sortino Ratio", "0"}, + {"Probabilistic Sharpe Ratio", "86.638%"}, + {"Loss Rate", "0%"}, + {"Win Rate", "0%"}, + {"Profit-Loss Ratio", "0"}, + {"Alpha", "-0.003"}, + {"Beta", "0.559"}, + {"Annual Standard Deviation", "0.028"}, + {"Annual Variance", "0.001"}, + {"Information Ratio", "-8.867"}, + {"Tracking Error", "0.023"}, + {"Treynor Ratio", "0.447"}, + {"Total Fees", "$1.95"}, + {"Estimated Strategy Capacity", "$850000000.00"}, + {"Lowest Capacity Asset", "SPY R735QTJ8XC9X"}, + {"Portfolio Turnover", "14.23%"}, + {"OrderListHash", "611f320cf76c36e8cdcb1938e4154682"} + }; + } +} diff --git a/Algorithm.CSharp/RegressionTests/LiquidateUsingSetHoldingsRegressionAlgorithm.cs b/Algorithm.CSharp/RegressionTests/LiquidateUsingSetHoldingsRegressionAlgorithm.cs index 5f60bbf7492f..d4166924bc4f 100644 --- a/Algorithm.CSharp/RegressionTests/LiquidateUsingSetHoldingsRegressionAlgorithm.cs +++ b/Algorithm.CSharp/RegressionTests/LiquidateUsingSetHoldingsRegressionAlgorithm.cs @@ -44,7 +44,7 @@ public override void PerformLiquidation() public override Dictionary ExpectedStatistics => new Dictionary { - {"Total Orders", "8"}, + {"Total Orders", "6"}, {"Average Win", "0%"}, {"Average Loss", "0%"}, {"Compounding Annual Return", "0%"}, @@ -70,7 +70,7 @@ public override void PerformLiquidation() {"Estimated Strategy Capacity", "$0"}, {"Lowest Capacity Asset", ""}, {"Portfolio Turnover", "0%"}, - {"OrderListHash", "25c4614305b6849ddec931fdf453af55"} + {"OrderListHash", "2cdbee112f22755f26f640c97c305aae"} }; } } diff --git a/Algorithm/QCAlgorithm.Trading.cs b/Algorithm/QCAlgorithm.Trading.cs index 866f23af59aa..8a6c613d3d10 100644 --- a/Algorithm/QCAlgorithm.Trading.cs +++ b/Algorithm/QCAlgorithm.Trading.cs @@ -1333,11 +1333,7 @@ public void SetHoldings(List targets, bool liquidateExistingHol //If they triggered a liquidate if (liquidateExistingHoldings) { - var targetSymbols = new HashSet(targets.Select(t => t.Symbol)); - var symbolsToLiquidate = Portfolio.Keys - .Where(symbol => !targetSymbols.Contains(symbol)) - .ToList(); - Liquidate(symbolsToLiquidate, tag: tag, orderProperties: orderProperties); + Liquidate(GetSymbolsToLiquidate(targets.Select(t => t.Symbol).ToList()), tag: tag, orderProperties: orderProperties); } foreach (var portfolioTarget in targets @@ -1359,7 +1355,7 @@ public void SetHoldings(List targets, bool liquidateExistingHol /// The order properties to use. Defaults to /// [DocumentationAttribute(TradingAndOrders)] - public void SetHoldings(Symbol symbol, double percentage, bool liquidateExistingHoldings = false, string tag = "", IOrderProperties orderProperties = null) + public void SetHoldings(Symbol symbol, double percentage, bool liquidateExistingHoldings = false, string tag = null, IOrderProperties orderProperties = null) { SetHoldings(symbol, percentage.SafeDecimalCast(), liquidateExistingHoldings, tag, orderProperties); } @@ -1374,7 +1370,7 @@ public void SetHoldings(Symbol symbol, double percentage, bool liquidateExisting /// The order properties to use. Defaults to /// [DocumentationAttribute(TradingAndOrders)] - public void SetHoldings(Symbol symbol, float percentage, bool liquidateExistingHoldings = false, string tag = "", IOrderProperties orderProperties = null) + public void SetHoldings(Symbol symbol, float percentage, bool liquidateExistingHoldings = false, string tag = null, IOrderProperties orderProperties = null) { SetHoldings(symbol, (decimal)percentage, liquidateExistingHoldings, tag, orderProperties); } @@ -1389,7 +1385,7 @@ public void SetHoldings(Symbol symbol, float percentage, bool liquidateExistingH /// The order properties to use. Defaults to /// [DocumentationAttribute(TradingAndOrders)] - public void SetHoldings(Symbol symbol, int percentage, bool liquidateExistingHoldings = false, string tag = "", IOrderProperties orderProperties = null) + public void SetHoldings(Symbol symbol, int percentage, bool liquidateExistingHoldings = false, string tag = null, IOrderProperties orderProperties = null) { SetHoldings(symbol, (decimal)percentage, liquidateExistingHoldings, tag, orderProperties); } @@ -1407,7 +1403,7 @@ public void SetHoldings(Symbol symbol, int percentage, bool liquidateExistingHol /// The order properties to use. Defaults to /// [DocumentationAttribute(TradingAndOrders)] - public void SetHoldings(Symbol symbol, decimal percentage, bool liquidateExistingHoldings = false, string tag = "", IOrderProperties orderProperties = null) + public void SetHoldings(Symbol symbol, decimal percentage, bool liquidateExistingHoldings = false, string tag = null, IOrderProperties orderProperties = null) { SetHoldingsImpl(symbol, CalculateOrderQuantity(symbol, percentage), liquidateExistingHoldings, tag, orderProperties); } @@ -1420,7 +1416,7 @@ private void SetHoldingsImpl(Symbol symbol, decimal orderQuantity, bool liquidat //If they triggered a liquidate if (liquidateExistingHoldings) { - Liquidate(symbol, tag: tag, orderProperties: orderProperties); + Liquidate(GetSymbolsToLiquidate(new List() { symbol }), tag: tag, orderProperties: orderProperties); } //Calculate total unfilled quantity for open market orders @@ -1444,15 +1440,29 @@ private void SetHoldingsImpl(Symbol symbol, decimal orderQuantity, bool liquidat //Check whether the exchange is open to send a market order. If not, send a market on open order instead if (security.Exchange.ExchangeOpen) { - MarketOrder(symbol, quantity, false, tag, orderProperties); + MarketOrder(symbol, quantity, false, tag ?? "", orderProperties); } else { - MarketOnOpenOrder(symbol, quantity, tag, orderProperties); + MarketOnOpenOrder(symbol, quantity, tag ?? "", orderProperties); } } } + /// + /// Returns the symbols in the portfolio to be liquidated, excluding the provided symbols. + /// + /// The list of symbols to exclude from liquidation. + /// A list of symbols to liquidate. + private List GetSymbolsToLiquidate(List symbols) + { + var targetSymbols = new HashSet(symbols); + var symbolsToLiquidate = Portfolio.Keys + .Where(symbol => !targetSymbols.Contains(symbol)) + .ToList(); + return symbolsToLiquidate; + } + /// /// Calculate the order quantity to achieve target-percent holdings. /// From eaafdca8a87a36f008a73e637f4c249abf3cb435 Mon Sep 17 00:00:00 2001 From: Josue Nina Date: Wed, 22 Jan 2025 09:52:35 -0500 Subject: [PATCH 8/9] Fix minor comments --- .../LiquidateRegressionAlgorithm.cs | 15 ++++++----- ...xceptSpecifiedSymbolRegressionAlgorithm.cs | 8 +++--- ...dateUsingSetHoldingsRegressionAlgorithm.cs | 25 +++++++++++-------- Algorithm/QCAlgorithm.Trading.cs | 12 +++++---- 4 files changed, 32 insertions(+), 28 deletions(-) diff --git a/Algorithm.CSharp/LiquidateRegressionAlgorithm.cs b/Algorithm.CSharp/LiquidateRegressionAlgorithm.cs index d6176fa06a1f..59d9756a239c 100644 --- a/Algorithm.CSharp/LiquidateRegressionAlgorithm.cs +++ b/Algorithm.CSharp/LiquidateRegressionAlgorithm.cs @@ -27,15 +27,14 @@ namespace QuantConnect.Algorithm.CSharp /// public class LiquidateRegressionAlgorithm : QCAlgorithm, IRegressionAlgorithmDefinition { - protected Symbol _spy; - protected Symbol _ibm; + protected Symbol Spy { get; private set; } + protected Symbol Ibm { get; private set; } public override void Initialize() { SetStartDate(2018, 1, 4); SetEndDate(2018, 1, 10); - SetCash(100000); - _spy = AddEquity("SPY", Resolution.Daily).Symbol; - _ibm = AddEquity("IBM", Resolution.Daily).Symbol; + Spy = AddEquity("SPY", Resolution.Daily).Symbol; + Ibm = AddEquity("IBM", Resolution.Daily).Symbol; // Schedule Rebalance method to be called on specific dates Schedule.On(DateRules.On(2018, 1, 5), TimeRules.Midnight, Rebalance); @@ -45,12 +44,12 @@ public override void Initialize() public virtual void Rebalance() { // Place a MarketOrder - MarketOrder(_ibm, 10); + MarketOrder(Ibm, 10); // Place a LimitOrder to sell 1 share at a price below the current market price - LimitOrder(_ibm, 1, Securities[_ibm].Price - 5); + LimitOrder(Ibm, 1, Securities[Ibm].Price - 5); - LimitOrder(_spy, 1, Securities[_spy].Price - 5); + LimitOrder(Spy, 1, Securities[Spy].Price - 5); // Liquidate all remaining holdings immediately PerformLiquidation(); diff --git a/Algorithm.CSharp/RegressionTests/LiquidateAllExceptSpecifiedSymbolRegressionAlgorithm.cs b/Algorithm.CSharp/RegressionTests/LiquidateAllExceptSpecifiedSymbolRegressionAlgorithm.cs index 567dd311a2f1..41dfaddb9bf9 100644 --- a/Algorithm.CSharp/RegressionTests/LiquidateAllExceptSpecifiedSymbolRegressionAlgorithm.cs +++ b/Algorithm.CSharp/RegressionTests/LiquidateAllExceptSpecifiedSymbolRegressionAlgorithm.cs @@ -28,14 +28,14 @@ public class LiquidateAllExceptSpecifiedSymbolRegressionAlgorithm : LiquidateReg public override void Rebalance() { // Place a MarketOrder - MarketOrder(_ibm, 10); + MarketOrder(Ibm, 10); // Place a LimitOrder to sell 1 share at a price below the current market price - LimitOrder(_ibm, 1, Securities[_ibm].Price - 5); + LimitOrder(Ibm, 1, Securities[Ibm].Price - 5); - // Liquidate SPY orders and verify cancellation + // Liquidate the remaining symbols in the portfolio, except for SPY var orderProperties = new OrderProperties { TimeInForce = TimeInForce.GoodTilCanceled }; - SetHoldings(_spy, 1, true, "LiquidatedTest", orderProperties); + SetHoldings(Spy, 1, true, "LiquidatedTest", orderProperties); } public override void OnEndOfAlgorithm() diff --git a/Algorithm.CSharp/RegressionTests/LiquidateUsingSetHoldingsRegressionAlgorithm.cs b/Algorithm.CSharp/RegressionTests/LiquidateUsingSetHoldingsRegressionAlgorithm.cs index d4166924bc4f..376707538c41 100644 --- a/Algorithm.CSharp/RegressionTests/LiquidateUsingSetHoldingsRegressionAlgorithm.cs +++ b/Algorithm.CSharp/RegressionTests/LiquidateUsingSetHoldingsRegressionAlgorithm.cs @@ -27,18 +27,21 @@ public class LiquidateUsingSetHoldingsRegressionAlgorithm : LiquidateRegressionA { public override void PerformLiquidation() { - var properties = new OrderProperties { TimeInForce = TimeInForce.GoodTilCanceled }; - SetHoldings(new List(), true, "LiquidatedTest", properties); - var orders = Transactions.GetOrders().ToList(); - var orderTags = orders.Where(e => e.Tag == "LiquidatedTest").ToList(); - if (orderTags.Count != orders.Count) + if (!Portfolio.Invested) { - throw new RegressionTestException("The tag was not set on all orders"); - } - var orderProperties = orders.Where(e => e.Properties.TimeInForce == TimeInForce.GoodTilCanceled).ToList(); - if (orderProperties.Count != orders.Count) - { - throw new RegressionTestException("The properties were not set on all orders"); + var properties = new OrderProperties { TimeInForce = TimeInForce.GoodTilCanceled }; + SetHoldings(new List(), true, "LiquidatedTest", properties); + var orders = Transactions.GetOrders().ToList(); + var orderTags = orders.Where(e => e.Tag == "LiquidatedTest").ToList(); + if (orderTags.Count != orders.Count) + { + throw new RegressionTestException("The tag was not set on all orders"); + } + var orderProperties = orders.Where(e => e.Properties.TimeInForce == TimeInForce.GoodTilCanceled).ToList(); + if (orderProperties.Count != orders.Count) + { + throw new RegressionTestException("The properties were not set on all orders"); + } } } diff --git a/Algorithm/QCAlgorithm.Trading.cs b/Algorithm/QCAlgorithm.Trading.cs index 8a6c613d3d10..dcd8262df3e9 100644 --- a/Algorithm/QCAlgorithm.Trading.cs +++ b/Algorithm/QCAlgorithm.Trading.cs @@ -1333,7 +1333,7 @@ public void SetHoldings(List targets, bool liquidateExistingHol //If they triggered a liquidate if (liquidateExistingHoldings) { - Liquidate(GetSymbolsToLiquidate(targets.Select(t => t.Symbol).ToList()), tag: tag, orderProperties: orderProperties); + Liquidate(GetSymbolsToLiquidate(targets.Select(t => t.Symbol)), tag: tag, orderProperties: orderProperties); } foreach (var portfolioTarget in targets @@ -1416,9 +1416,10 @@ private void SetHoldingsImpl(Symbol symbol, decimal orderQuantity, bool liquidat //If they triggered a liquidate if (liquidateExistingHoldings) { - Liquidate(GetSymbolsToLiquidate(new List() { symbol }), tag: tag, orderProperties: orderProperties); + Liquidate(GetSymbolsToLiquidate([symbol]), tag: tag, orderProperties: orderProperties); } + tag ??= ""; //Calculate total unfilled quantity for open market orders var marketOrdersQuantity = Transactions.GetOpenOrderTickets( ticket => ticket.Symbol == symbol @@ -1440,11 +1441,11 @@ private void SetHoldingsImpl(Symbol symbol, decimal orderQuantity, bool liquidat //Check whether the exchange is open to send a market order. If not, send a market on open order instead if (security.Exchange.ExchangeOpen) { - MarketOrder(symbol, quantity, false, tag ?? "", orderProperties); + MarketOrder(symbol, quantity, false, tag, orderProperties); } else { - MarketOnOpenOrder(symbol, quantity, tag ?? "", orderProperties); + MarketOnOpenOrder(symbol, quantity, tag, orderProperties); } } } @@ -1454,11 +1455,12 @@ private void SetHoldingsImpl(Symbol symbol, decimal orderQuantity, bool liquidat /// /// The list of symbols to exclude from liquidation. /// A list of symbols to liquidate. - private List GetSymbolsToLiquidate(List symbols) + private List GetSymbolsToLiquidate(IEnumerable symbols) { var targetSymbols = new HashSet(symbols); var symbolsToLiquidate = Portfolio.Keys .Where(symbol => !targetSymbols.Contains(symbol)) + .OrderBy(symbol => symbol.Value) .ToList(); return symbolsToLiquidate; } From 652a19d8cade39e27fc0325966f8a3c71050b224 Mon Sep 17 00:00:00 2001 From: Josue Nina Date: Wed, 22 Jan 2025 10:25:18 -0500 Subject: [PATCH 9/9] Move regression tests to the correct folder --- ...xceptSpecifiedSymbolRegressionAlgorithm.cs | 0 ...dateUsingSetHoldingsRegressionAlgorithm.cs | 25 ++++++++----------- 2 files changed, 11 insertions(+), 14 deletions(-) rename Algorithm.CSharp/{RegressionTests => }/LiquidateAllExceptSpecifiedSymbolRegressionAlgorithm.cs (100%) rename Algorithm.CSharp/{RegressionTests => }/LiquidateUsingSetHoldingsRegressionAlgorithm.cs (73%) diff --git a/Algorithm.CSharp/RegressionTests/LiquidateAllExceptSpecifiedSymbolRegressionAlgorithm.cs b/Algorithm.CSharp/LiquidateAllExceptSpecifiedSymbolRegressionAlgorithm.cs similarity index 100% rename from Algorithm.CSharp/RegressionTests/LiquidateAllExceptSpecifiedSymbolRegressionAlgorithm.cs rename to Algorithm.CSharp/LiquidateAllExceptSpecifiedSymbolRegressionAlgorithm.cs diff --git a/Algorithm.CSharp/RegressionTests/LiquidateUsingSetHoldingsRegressionAlgorithm.cs b/Algorithm.CSharp/LiquidateUsingSetHoldingsRegressionAlgorithm.cs similarity index 73% rename from Algorithm.CSharp/RegressionTests/LiquidateUsingSetHoldingsRegressionAlgorithm.cs rename to Algorithm.CSharp/LiquidateUsingSetHoldingsRegressionAlgorithm.cs index 376707538c41..d4166924bc4f 100644 --- a/Algorithm.CSharp/RegressionTests/LiquidateUsingSetHoldingsRegressionAlgorithm.cs +++ b/Algorithm.CSharp/LiquidateUsingSetHoldingsRegressionAlgorithm.cs @@ -27,21 +27,18 @@ public class LiquidateUsingSetHoldingsRegressionAlgorithm : LiquidateRegressionA { public override void PerformLiquidation() { - if (!Portfolio.Invested) + var properties = new OrderProperties { TimeInForce = TimeInForce.GoodTilCanceled }; + SetHoldings(new List(), true, "LiquidatedTest", properties); + var orders = Transactions.GetOrders().ToList(); + var orderTags = orders.Where(e => e.Tag == "LiquidatedTest").ToList(); + if (orderTags.Count != orders.Count) { - var properties = new OrderProperties { TimeInForce = TimeInForce.GoodTilCanceled }; - SetHoldings(new List(), true, "LiquidatedTest", properties); - var orders = Transactions.GetOrders().ToList(); - var orderTags = orders.Where(e => e.Tag == "LiquidatedTest").ToList(); - if (orderTags.Count != orders.Count) - { - throw new RegressionTestException("The tag was not set on all orders"); - } - var orderProperties = orders.Where(e => e.Properties.TimeInForce == TimeInForce.GoodTilCanceled).ToList(); - if (orderProperties.Count != orders.Count) - { - throw new RegressionTestException("The properties were not set on all orders"); - } + throw new RegressionTestException("The tag was not set on all orders"); + } + var orderProperties = orders.Where(e => e.Properties.TimeInForce == TimeInForce.GoodTilCanceled).ToList(); + if (orderProperties.Count != orders.Count) + { + throw new RegressionTestException("The properties were not set on all orders"); } }