Skip to content

Commit 9b35411

Browse files
authored
Refactor liquidation logic (#8544)
* 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 * Addressed review comments * Addressed new comments review * Update default value for 'tag' * Update ExpectedStatistics * Identify and liquidate portfolio symbols not included in targets * Create a new regression test * Fix minor comments * Move regression tests to the correct folder
1 parent 2fe7f5b commit 9b35411

File tree

4 files changed

+355
-32
lines changed

4 files changed

+355
-32
lines changed
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/*
2+
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
3+
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
using System;
17+
using System.Collections.Generic;
18+
using System.Linq;
19+
using QuantConnect.Orders;
20+
21+
namespace QuantConnect.Algorithm.CSharp.RegressionTests
22+
{
23+
/// <summary>
24+
/// Tests liquidating all portfolio holdings except a specific symbol, verifying canceled orders and correct tags.
25+
/// </summary>
26+
public class LiquidateAllExceptSpecifiedSymbolRegressionAlgorithm : LiquidateRegressionAlgorithm
27+
{
28+
public override void Rebalance()
29+
{
30+
// Place a MarketOrder
31+
MarketOrder(Ibm, 10);
32+
33+
// Place a LimitOrder to sell 1 share at a price below the current market price
34+
LimitOrder(Ibm, 1, Securities[Ibm].Price - 5);
35+
36+
// Liquidate the remaining symbols in the portfolio, except for SPY
37+
var orderProperties = new OrderProperties { TimeInForce = TimeInForce.GoodTilCanceled };
38+
SetHoldings(Spy, 1, true, "LiquidatedTest", orderProperties);
39+
}
40+
41+
public override void OnEndOfAlgorithm()
42+
{
43+
// Retrieve all orders from the Transactions for analysis
44+
var orders = Transactions.GetOrders().ToList();
45+
46+
// Count orders that were canceled
47+
var canceledOrdersCount = orders.Where(order => order.Status == OrderStatus.Canceled).Count();
48+
49+
// Expectation 1: There should be exactly 4 canceled orders.
50+
// This occurs because Rebalance is called twice, and each call to Rebalance
51+
// (e.g., LimitOrder or MarketOrder) that get canceled due to the Liquidate call in SetHoldings.
52+
if (canceledOrdersCount != 4)
53+
{
54+
throw new RegressionTestException($"Expected 4 canceled orders, but found {canceledOrdersCount}.");
55+
}
56+
57+
// Count orders that were not canceled
58+
var nonCanceledOrdersCount = orders.Where(order => order.Status != OrderStatus.Canceled).Count();
59+
60+
// Expectation 2: There should be exactly 1 non-canceled order after the Liquidate call.
61+
// This occurs because all holdings except SPY are liquidated, and a new order is placed for SPY.
62+
if (nonCanceledOrdersCount != 1)
63+
{
64+
throw new RegressionTestException($"Expected 1 non-canceled order, but found {nonCanceledOrdersCount}.");
65+
}
66+
67+
// Verify all tags are "LiquidatedTest"
68+
var invalidTags = orders.Where(order => order.Tag != "LiquidatedTest").ToList();
69+
if (invalidTags.Count != 0)
70+
{
71+
var invalidTagsDetails = string.Join(", ", invalidTags.Select(order => $"OrderID {order.Id}, Tag: {order.Tag}"));
72+
throw new RegressionTestException($"All orders should have the tag 'LiquidatedTest', but found invalid tags: {invalidTagsDetails}.");
73+
}
74+
}
75+
76+
public override Dictionary<string, string> ExpectedStatistics => new Dictionary<string, string>
77+
{
78+
{"Total Orders", "5"},
79+
{"Average Win", "0%"},
80+
{"Average Loss", "0%"},
81+
{"Compounding Annual Return", "36.497%"},
82+
{"Drawdown", "0.200%"},
83+
{"Expectancy", "0"},
84+
{"Start Equity", "100000"},
85+
{"End Equity", "100569.90"},
86+
{"Net Profit", "0.570%"},
87+
{"Sharpe Ratio", "9.031"},
88+
{"Sortino Ratio", "0"},
89+
{"Probabilistic Sharpe Ratio", "86.638%"},
90+
{"Loss Rate", "0%"},
91+
{"Win Rate", "0%"},
92+
{"Profit-Loss Ratio", "0"},
93+
{"Alpha", "-0.003"},
94+
{"Beta", "0.559"},
95+
{"Annual Standard Deviation", "0.028"},
96+
{"Annual Variance", "0.001"},
97+
{"Information Ratio", "-8.867"},
98+
{"Tracking Error", "0.023"},
99+
{"Treynor Ratio", "0.447"},
100+
{"Total Fees", "$1.95"},
101+
{"Estimated Strategy Capacity", "$850000000.00"},
102+
{"Lowest Capacity Asset", "SPY R735QTJ8XC9X"},
103+
{"Portfolio Turnover", "14.23%"},
104+
{"OrderListHash", "611f320cf76c36e8cdcb1938e4154682"}
105+
};
106+
}
107+
}
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
/*
2+
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
3+
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
using System;
17+
using System.Collections.Generic;
18+
using System.Linq;
19+
using QuantConnect.Interfaces;
20+
using QuantConnect.Orders;
21+
22+
namespace QuantConnect.Algorithm.CSharp
23+
{
24+
/// <summary>
25+
/// A regression test algorithm that places market and limit orders, then liquidates all holdings,
26+
/// ensuring orders are canceled and the portfolio is empty.
27+
/// </summary>
28+
public class LiquidateRegressionAlgorithm : QCAlgorithm, IRegressionAlgorithmDefinition
29+
{
30+
protected Symbol Spy { get; private set; }
31+
protected Symbol Ibm { get; private set; }
32+
public override void Initialize()
33+
{
34+
SetStartDate(2018, 1, 4);
35+
SetEndDate(2018, 1, 10);
36+
Spy = AddEquity("SPY", Resolution.Daily).Symbol;
37+
Ibm = AddEquity("IBM", Resolution.Daily).Symbol;
38+
39+
// Schedule Rebalance method to be called on specific dates
40+
Schedule.On(DateRules.On(2018, 1, 5), TimeRules.Midnight, Rebalance);
41+
Schedule.On(DateRules.On(2018, 1, 8), TimeRules.Midnight, Rebalance);
42+
}
43+
44+
public virtual void Rebalance()
45+
{
46+
// Place a MarketOrder
47+
MarketOrder(Ibm, 10);
48+
49+
// Place a LimitOrder to sell 1 share at a price below the current market price
50+
LimitOrder(Ibm, 1, Securities[Ibm].Price - 5);
51+
52+
LimitOrder(Spy, 1, Securities[Spy].Price - 5);
53+
54+
// Liquidate all remaining holdings immediately
55+
PerformLiquidation();
56+
}
57+
58+
public virtual void PerformLiquidation()
59+
{
60+
Liquidate();
61+
}
62+
63+
public override void OnEndOfAlgorithm()
64+
{
65+
// Check if there are any orders that should have been canceled
66+
var orders = Transactions.GetOrders().ToList();
67+
var nonCanceledOrdersCount = orders.Where(e => e.Status != OrderStatus.Canceled).Count();
68+
if (nonCanceledOrdersCount > 0)
69+
{
70+
throw new RegressionTestException($"There are {nonCanceledOrdersCount} orders that should have been cancelled");
71+
}
72+
73+
// Check if there are any holdings left in the portfolio
74+
foreach (var kvp in Portfolio)
75+
{
76+
var symbol = kvp.Key;
77+
var holdings = kvp.Value;
78+
if (holdings.Quantity != 0)
79+
{
80+
throw new RegressionTestException($"There are {holdings.Quantity} holdings of {symbol} in the portfolio");
81+
}
82+
}
83+
}
84+
85+
/// <summary>
86+
/// Final status of the algorithm
87+
/// </summary>
88+
public AlgorithmStatus AlgorithmStatus => AlgorithmStatus.Completed;
89+
90+
/// <summary>
91+
/// This is used by the regression test system to indicate if the open source Lean repository has the required data to run this algorithm.
92+
/// </summary>
93+
public bool CanRunLocally { get; } = true;
94+
95+
/// <summary>
96+
/// This is used by the regression test system to indicate which languages this algorithm is written in.
97+
/// </summary>
98+
public List<Language> Languages { get; } = new() { Language.CSharp };
99+
100+
/// <summary>
101+
/// Data Points count of all timeslices of algorithm
102+
/// </summary>
103+
public long DataPoints => 53;
104+
105+
/// <summary>
106+
/// Data Points count of the algorithm history
107+
/// </summary>
108+
public int AlgorithmHistoryDataPoints => 0;
109+
110+
/// <summary>
111+
/// This is used by the regression test system to indicate what the expected statistics are from running the algorithm
112+
/// </summary>
113+
public virtual Dictionary<string, string> ExpectedStatistics => new Dictionary<string, string>
114+
{
115+
{"Total Orders", "6"},
116+
{"Average Win", "0%"},
117+
{"Average Loss", "0%"},
118+
{"Compounding Annual Return", "0%"},
119+
{"Drawdown", "0%"},
120+
{"Expectancy", "0"},
121+
{"Start Equity", "100000"},
122+
{"End Equity", "100000"},
123+
{"Net Profit", "0%"},
124+
{"Sharpe Ratio", "0"},
125+
{"Sortino Ratio", "0"},
126+
{"Probabilistic Sharpe Ratio", "0%"},
127+
{"Loss Rate", "0%"},
128+
{"Win Rate", "0%"},
129+
{"Profit-Loss Ratio", "0"},
130+
{"Alpha", "0"},
131+
{"Beta", "0"},
132+
{"Annual Standard Deviation", "0"},
133+
{"Annual Variance", "0"},
134+
{"Information Ratio", "-10.398"},
135+
{"Tracking Error", "0.045"},
136+
{"Treynor Ratio", "0"},
137+
{"Total Fees", "$0.00"},
138+
{"Estimated Strategy Capacity", "$0"},
139+
{"Lowest Capacity Asset", ""},
140+
{"Portfolio Turnover", "0%"},
141+
{"OrderListHash", "9423c872a626fb856b7c377686c28d85"}
142+
};
143+
}
144+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
3+
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
using System.Collections.Generic;
17+
using System.Linq;
18+
using QuantConnect.Algorithm.Framework.Portfolio;
19+
using QuantConnect.Orders;
20+
21+
namespace QuantConnect.Algorithm.CSharp.RegressionTests
22+
{
23+
/// <summary>
24+
/// A regression test algorithm that uses SetHoldings to liquidate the portfolio by setting holdings to zero.
25+
/// </summary>
26+
public class LiquidateUsingSetHoldingsRegressionAlgorithm : LiquidateRegressionAlgorithm
27+
{
28+
public override void PerformLiquidation()
29+
{
30+
var properties = new OrderProperties { TimeInForce = TimeInForce.GoodTilCanceled };
31+
SetHoldings(new List<PortfolioTarget>(), true, "LiquidatedTest", properties);
32+
var orders = Transactions.GetOrders().ToList();
33+
var orderTags = orders.Where(e => e.Tag == "LiquidatedTest").ToList();
34+
if (orderTags.Count != orders.Count)
35+
{
36+
throw new RegressionTestException("The tag was not set on all orders");
37+
}
38+
var orderProperties = orders.Where(e => e.Properties.TimeInForce == TimeInForce.GoodTilCanceled).ToList();
39+
if (orderProperties.Count != orders.Count)
40+
{
41+
throw new RegressionTestException("The properties were not set on all orders");
42+
}
43+
}
44+
45+
public override Dictionary<string, string> ExpectedStatistics => new Dictionary<string, string>
46+
{
47+
{"Total Orders", "6"},
48+
{"Average Win", "0%"},
49+
{"Average Loss", "0%"},
50+
{"Compounding Annual Return", "0%"},
51+
{"Drawdown", "0%"},
52+
{"Expectancy", "0"},
53+
{"Start Equity", "100000"},
54+
{"End Equity", "100000"},
55+
{"Net Profit", "0%"},
56+
{"Sharpe Ratio", "0"},
57+
{"Sortino Ratio", "0"},
58+
{"Probabilistic Sharpe Ratio", "0%"},
59+
{"Loss Rate", "0%"},
60+
{"Win Rate", "0%"},
61+
{"Profit-Loss Ratio", "0"},
62+
{"Alpha", "0"},
63+
{"Beta", "0"},
64+
{"Annual Standard Deviation", "0"},
65+
{"Annual Variance", "0"},
66+
{"Information Ratio", "-10.398"},
67+
{"Tracking Error", "0.045"},
68+
{"Treynor Ratio", "0"},
69+
{"Total Fees", "$0.00"},
70+
{"Estimated Strategy Capacity", "$0"},
71+
{"Lowest Capacity Asset", ""},
72+
{"Portfolio Turnover", "0%"},
73+
{"OrderListHash", "2cdbee112f22755f26f640c97c305aae"}
74+
};
75+
}
76+
}

0 commit comments

Comments
 (0)