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; } ///