diff --git a/Algorithm.CSharp/LiquidateAllExceptSpecifiedSymbolRegressionAlgorithm.cs b/Algorithm.CSharp/LiquidateAllExceptSpecifiedSymbolRegressionAlgorithm.cs
new file mode 100644
index 000000000000..41dfaddb9bf9
--- /dev/null
+++ b/Algorithm.CSharp/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 the remaining symbols in the portfolio, except for SPY
+ 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/LiquidateRegressionAlgorithm.cs b/Algorithm.CSharp/LiquidateRegressionAlgorithm.cs
new file mode 100644
index 000000000000..59d9756a239c
--- /dev/null
+++ b/Algorithm.CSharp/LiquidateRegressionAlgorithm.cs
@@ -0,0 +1,144 @@
+/*
+ * 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.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 { get; private set; }
+ protected Symbol Ibm { get; private set; }
+ public override void Initialize()
+ {
+ SetStartDate(2018, 1, 4);
+ SetEndDate(2018, 1, 10);
+ 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);
+ 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 remaining 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 nonCanceledOrdersCount = orders.Where(e => e.Status != OrderStatus.Canceled).Count();
+ if (nonCanceledOrdersCount > 0)
+ {
+ throw new RegressionTestException($"There are {nonCanceledOrdersCount} 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.Quantity} holdings of {symbol} in the 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 => 53;
+
+ ///
+ /// 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 virtual Dictionary ExpectedStatistics => new Dictionary
+ {
+ {"Total Orders", "6"},
+ {"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", "9423c872a626fb856b7c377686c28d85"}
+ };
+ }
+}
diff --git a/Algorithm.CSharp/LiquidateUsingSetHoldingsRegressionAlgorithm.cs b/Algorithm.CSharp/LiquidateUsingSetHoldingsRegressionAlgorithm.cs
new file mode 100644
index 000000000000..d4166924bc4f
--- /dev/null
+++ b/Algorithm.CSharp/LiquidateUsingSetHoldingsRegressionAlgorithm.cs
@@ -0,0 +1,76 @@
+/*
+ * 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 QuantConnect.Algorithm.Framework.Portfolio;
+using QuantConnect.Orders;
+
+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()
+ {
+ 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", "6"},
+ {"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", "2cdbee112f22755f26f640c97c305aae"}
+ };
+ }
+}
diff --git a/Algorithm/QCAlgorithm.Trading.cs b/Algorithm/QCAlgorithm.Trading.cs
index 1ef0ba2c53e5..dcd8262df3e9 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;
}
@@ -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)
@@ -1237,7 +1237,8 @@ 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;
}
-
+
+ tag ??= "Liquidated";
foreach (var symbolToLiquidate in symbols)
{
// get open orders
@@ -1300,7 +1301,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();
}
///
@@ -1327,18 +1328,18 @@ 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)
{
- LiquidateExistingHoldings(targets.Select(x => x.Symbol).ToHashSet(), tag, orderProperties);
+ Liquidate(GetSymbolsToLiquidate(targets.Select(t => t.Symbol)), tag: tag, orderProperties: orderProperties);
}
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);
}
@@ -1354,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);
}
@@ -1369,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);
}
@@ -1384,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);
}
@@ -1402,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);
}
@@ -1410,14 +1411,15 @@ 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)
{
- LiquidateExistingHoldings(new HashSet { symbol }, tag, 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
@@ -1449,24 +1451,18 @@ private void SetHoldingsImpl(Symbol symbol, decimal orderQuantity, bool liquidat
}
///
- /// Liquidate existing holdings, except for the target list of Symbol.
+ /// Returns the symbols in the portfolio to be liquidated, excluding the provided symbols.
///
- /// 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)
+ /// The list of symbols to exclude from liquidation.
+ /// A list of symbols to liquidate.
+ private List GetSymbolsToLiquidate(IEnumerable symbols)
{
- 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);
- }
- }
+ var targetSymbols = new HashSet(symbols);
+ var symbolsToLiquidate = Portfolio.Keys
+ .Where(symbol => !targetSymbols.Contains(symbol))
+ .OrderBy(symbol => symbol.Value)
+ .ToList();
+ return symbolsToLiquidate;
}
///