Skip to content

Commit cad5f20

Browse files
authored
Refactor SetHoldings to return a List<OrderTicket> (QuantConnect#8550)
* Refactor SetHoldings to return a List<OrderTicket> * Add expectedQuantities to the testCases * Update regression tests * Update name of regressionAlgorithm * Address review comments * Update unit test
1 parent c5c56bf commit cad5f20

6 files changed

+224
-18
lines changed

Algorithm.CSharp/LiquidateAllExceptSpecifiedSymbolRegressionAlgorithm.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public override void Rebalance()
3535

3636
// Liquidate the remaining symbols in the portfolio, except for SPY
3737
var orderProperties = new OrderProperties { TimeInForce = TimeInForce.GoodTilCanceled };
38-
SetHoldings(Spy, 1, true, "LiquidatedTest", orderProperties);
38+
OrderTickets.AddRange(SetHoldings(Spy, 1, true, "LiquidatedTest", orderProperties));
3939
}
4040

4141
public override void OnEndOfAlgorithm()
@@ -64,6 +64,11 @@ public override void OnEndOfAlgorithm()
6464
throw new RegressionTestException($"Expected 1 non-canceled order, but found {nonCanceledOrdersCount}.");
6565
}
6666

67+
if (nonCanceledOrdersCount != OrderTickets.Count)
68+
{
69+
throw new RegressionTestException($"Expected {OrderTickets.Count} non-canceled orders, but found {nonCanceledOrdersCount}.");
70+
}
71+
6772
// Verify all tags are "LiquidatedTest"
6873
var invalidTags = orders.Where(order => order.Tag != "LiquidatedTest").ToList();
6974
if (invalidTags.Count != 0)

Algorithm.CSharp/LiquidateRegressionAlgorithm.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ namespace QuantConnect.Algorithm.CSharp
2727
/// </summary>
2828
public class LiquidateRegressionAlgorithm : QCAlgorithm, IRegressionAlgorithmDefinition
2929
{
30+
protected List<OrderTicket> OrderTickets { get; private set; }
3031
protected Symbol Spy { get; private set; }
3132
protected Symbol Ibm { get; private set; }
3233
public override void Initialize()
@@ -35,6 +36,7 @@ public override void Initialize()
3536
SetEndDate(2018, 1, 10);
3637
Spy = AddEquity("SPY", Resolution.Daily).Symbol;
3738
Ibm = AddEquity("IBM", Resolution.Daily).Symbol;
39+
OrderTickets = new List<OrderTicket>();
3840

3941
// Schedule Rebalance method to be called on specific dates
4042
Schedule.On(DateRules.On(2018, 1, 5), TimeRules.Midnight, Rebalance);
@@ -70,6 +72,11 @@ public override void OnEndOfAlgorithm()
7072
throw new RegressionTestException($"There are {nonCanceledOrdersCount} orders that should have been cancelled");
7173
}
7274

75+
if (OrderTickets.Count > 0)
76+
{
77+
throw new RegressionTestException("The number of order tickets must be zero because all orders were cancelled");
78+
}
79+
7380
// Check if there are any holdings left in the portfolio
7481
foreach (var kvp in Portfolio)
7582
{

Algorithm.CSharp/LiquidateUsingSetHoldingsRegressionAlgorithm.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public class LiquidateUsingSetHoldingsRegressionAlgorithm : LiquidateRegressionA
2828
public override void PerformLiquidation()
2929
{
3030
var properties = new OrderProperties { TimeInForce = TimeInForce.GoodTilCanceled };
31-
SetHoldings(new List<PortfolioTarget>(), true, "LiquidatedTest", properties);
31+
OrderTickets.AddRange(SetHoldings(new List<PortfolioTarget>(), true, "LiquidatedTest", properties));
3232
var orders = Transactions.GetOrders().ToList();
3333
var orderTags = orders.Where(e => e.Tag == "LiquidatedTest").ToList();
3434
if (orderTags.Count != orders.Count)
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
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 QuantConnect.Algorithm.Framework.Portfolio;
18+
using QuantConnect.Data;
19+
using QuantConnect.Interfaces;
20+
21+
namespace QuantConnect.Algorithm.CSharp
22+
{
23+
/// <summary>
24+
/// Validates that SetHoldings returns the correct number of order tickets on each execution.
25+
/// </summary>
26+
public class SetHoldingReturnsOrderTicketsRegressionAlgorithm : QCAlgorithm, IRegressionAlgorithmDefinition
27+
{
28+
private Symbol _spy;
29+
private Symbol _ibm;
30+
public override void Initialize()
31+
{
32+
SetStartDate(2018, 1, 4);
33+
SetEndDate(2018, 1, 10);
34+
_spy = AddEquity("SPY", Resolution.Daily).Symbol;
35+
_ibm = AddEquity("IBM", Resolution.Daily).Symbol;
36+
}
37+
38+
public override void OnData(Slice slice)
39+
{
40+
var tickets = SetHoldings(new List<PortfolioTarget> { new(_spy, 0.8m), new(_ibm, 0.2m) });
41+
42+
if (!Portfolio.Invested)
43+
{
44+
// Ensure exactly 2 tickets are created when the portfolio is not yet invested
45+
if (tickets.Count != 2)
46+
{
47+
throw new RegressionTestException("Expected 2 tickets, got " + tickets.Count);
48+
}
49+
}
50+
else if (tickets.Count != 0)
51+
{
52+
// Ensure no tickets are created when the portfolio is already invested
53+
throw new RegressionTestException("Expected 0 tickets, got " + tickets.Count);
54+
}
55+
}
56+
57+
/// <summary>
58+
/// Final status of the algorithm
59+
/// </summary>
60+
public AlgorithmStatus AlgorithmStatus => AlgorithmStatus.Completed;
61+
62+
/// <summary>
63+
/// This is used by the regression test system to indicate if the open source Lean repository has the required data to run this algorithm.
64+
/// </summary>
65+
public bool CanRunLocally { get; } = true;
66+
67+
/// <summary>
68+
/// This is used by the regression test system to indicate which languages this algorithm is written in.
69+
/// </summary>
70+
public List<Language> Languages { get; } = new() { Language.CSharp };
71+
72+
/// <summary>
73+
/// Data Points count of all timeslices of algorithm
74+
/// </summary>
75+
public long DataPoints => 53;
76+
77+
/// <summary>
78+
/// Data Points count of the algorithm history
79+
/// </summary>
80+
public int AlgorithmHistoryDataPoints => 0;
81+
82+
/// <summary>
83+
/// This is used by the regression test system to indicate what the expected statistics are from running the algorithm
84+
/// </summary>
85+
public Dictionary<string, string> ExpectedStatistics => new Dictionary<string, string>
86+
{
87+
{"Total Orders", "2"},
88+
{"Average Win", "0%"},
89+
{"Average Loss", "0%"},
90+
{"Compounding Annual Return", "43.490%"},
91+
{"Drawdown", "0.100%"},
92+
{"Expectancy", "0"},
93+
{"Start Equity", "100000"},
94+
{"End Equity", "100661.71"},
95+
{"Net Profit", "0.662%"},
96+
{"Sharpe Ratio", "12.329"},
97+
{"Sortino Ratio", "0"},
98+
{"Probabilistic Sharpe Ratio", "97.100%"},
99+
{"Loss Rate", "0%"},
100+
{"Win Rate", "0%"},
101+
{"Profit-Loss Ratio", "0"},
102+
{"Alpha", "0.108"},
103+
{"Beta", "0.424"},
104+
{"Annual Standard Deviation", "0.024"},
105+
{"Annual Variance", "0.001"},
106+
{"Information Ratio", "-5.097"},
107+
{"Tracking Error", "0.03"},
108+
{"Treynor Ratio", "0.707"},
109+
{"Total Fees", "$2.56"},
110+
{"Estimated Strategy Capacity", "$170000000.00"},
111+
{"Lowest Capacity Asset", "IBM R735QTJ8XC9X"},
112+
{"Portfolio Turnover", "14.24%"},
113+
{"OrderListHash", "587e1a69d3c83cbd9907f9f9586697e1"}
114+
};
115+
}
116+
}

Algorithm/QCAlgorithm.Trading.cs

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1326,23 +1326,28 @@ public void SetMaximumOrders(int max)
13261326
/// <param name="liquidateExistingHoldings">True will liquidate existing holdings</param>
13271327
/// <param name="tag">Tag the order with a short string.</param>
13281328
/// <param name="orderProperties">The order properties to use. Defaults to <see cref="DefaultOrderProperties"/></param>
1329+
/// <returns>A list of order tickets.</returns>
13291330
/// <seealso cref="MarketOrder(QuantConnect.Symbol, decimal, bool, string, IOrderProperties)"/>
13301331
[DocumentationAttribute(TradingAndOrders)]
1331-
public void SetHoldings(List<PortfolioTarget> targets, bool liquidateExistingHoldings = false, string tag = null, IOrderProperties orderProperties = null)
1332+
public List<OrderTicket> SetHoldings(List<PortfolioTarget> targets, bool liquidateExistingHoldings = false, string tag = null, IOrderProperties orderProperties = null)
13321333
{
1334+
List<OrderTicket> orderTickets = null;
13331335
//If they triggered a liquidate
13341336
if (liquidateExistingHoldings)
13351337
{
1336-
Liquidate(GetSymbolsToLiquidate(targets.Select(t => t.Symbol)), tag: tag, orderProperties: orderProperties);
1338+
orderTickets = Liquidate(GetSymbolsToLiquidate(targets.Select(t => t.Symbol)), tag: tag, orderProperties: orderProperties);
13371339
}
1340+
orderTickets ??= new List<OrderTicket>();
13381341

13391342
foreach (var portfolioTarget in targets
13401343
// we need to create targets with quantities for OrderTargetsByMarginImpact
13411344
.Select(target => new PortfolioTarget(target.Symbol, CalculateOrderQuantity(target.Symbol, target.Quantity)))
13421345
.OrderTargetsByMarginImpact(this, targetIsDelta: true))
13431346
{
1344-
SetHoldingsImpl(portfolioTarget.Symbol, portfolioTarget.Quantity, false, tag, orderProperties);
1347+
var tickets = SetHoldingsImpl(portfolioTarget.Symbol, portfolioTarget.Quantity, false, tag, orderProperties);
1348+
orderTickets.AddRange(tickets);
13451349
}
1350+
return orderTickets;
13461351
}
13471352

13481353
/// <summary>
@@ -1353,11 +1358,12 @@ public void SetHoldings(List<PortfolioTarget> targets, bool liquidateExistingHol
13531358
/// <param name="liquidateExistingHoldings">liquidate existing holdings if necessary to hold this stock</param>
13541359
/// <param name="tag">Tag the order with a short string.</param>
13551360
/// <param name="orderProperties">The order properties to use. Defaults to <see cref="DefaultOrderProperties"/></param>
1361+
/// <returns>A list of order tickets.</returns>
13561362
/// <seealso cref="MarketOrder(QuantConnect.Symbol, decimal, bool, string, IOrderProperties)"/>
13571363
[DocumentationAttribute(TradingAndOrders)]
1358-
public void SetHoldings(Symbol symbol, double percentage, bool liquidateExistingHoldings = false, string tag = null, IOrderProperties orderProperties = null)
1364+
public List<OrderTicket> SetHoldings(Symbol symbol, double percentage, bool liquidateExistingHoldings = false, string tag = null, IOrderProperties orderProperties = null)
13591365
{
1360-
SetHoldings(symbol, percentage.SafeDecimalCast(), liquidateExistingHoldings, tag, orderProperties);
1366+
return SetHoldings(symbol, percentage.SafeDecimalCast(), liquidateExistingHoldings, tag, orderProperties);
13611367
}
13621368

13631369
/// <summary>
@@ -1368,11 +1374,12 @@ public void SetHoldings(Symbol symbol, double percentage, bool liquidateExisting
13681374
/// <param name="liquidateExistingHoldings">bool liquidate existing holdings if necessary to hold this stock</param>
13691375
/// <param name="tag">Tag the order with a short string.</param>
13701376
/// <param name="orderProperties">The order properties to use. Defaults to <see cref="DefaultOrderProperties"/></param>
1377+
/// <returns>A list of order tickets.</returns>
13711378
/// <seealso cref="MarketOrder(QuantConnect.Symbol, decimal, bool, string, IOrderProperties)"/>
13721379
[DocumentationAttribute(TradingAndOrders)]
1373-
public void SetHoldings(Symbol symbol, float percentage, bool liquidateExistingHoldings = false, string tag = null, IOrderProperties orderProperties = null)
1380+
public List<OrderTicket> SetHoldings(Symbol symbol, float percentage, bool liquidateExistingHoldings = false, string tag = null, IOrderProperties orderProperties = null)
13741381
{
1375-
SetHoldings(symbol, (decimal)percentage, liquidateExistingHoldings, tag, orderProperties);
1382+
return SetHoldings(symbol, (decimal)percentage, liquidateExistingHoldings, tag, orderProperties);
13761383
}
13771384

13781385
/// <summary>
@@ -1383,11 +1390,12 @@ public void SetHoldings(Symbol symbol, float percentage, bool liquidateExistingH
13831390
/// <param name="liquidateExistingHoldings">bool liquidate existing holdings if necessary to hold this stock</param>
13841391
/// <param name="tag">Tag the order with a short string.</param>
13851392
/// <param name="orderProperties">The order properties to use. Defaults to <see cref="DefaultOrderProperties"/></param>
1393+
/// <returns>A list of order tickets.</returns>
13861394
/// <seealso cref="MarketOrder(QuantConnect.Symbol, decimal, bool, string, IOrderProperties)"/>
13871395
[DocumentationAttribute(TradingAndOrders)]
1388-
public void SetHoldings(Symbol symbol, int percentage, bool liquidateExistingHoldings = false, string tag = null, IOrderProperties orderProperties = null)
1396+
public List<OrderTicket> SetHoldings(Symbol symbol, int percentage, bool liquidateExistingHoldings = false, string tag = null, IOrderProperties orderProperties = null)
13891397
{
1390-
SetHoldings(symbol, (decimal)percentage, liquidateExistingHoldings, tag, orderProperties);
1398+
return SetHoldings(symbol, (decimal)percentage, liquidateExistingHoldings, tag, orderProperties);
13911399
}
13921400

13931401
/// <summary>
@@ -1401,24 +1409,27 @@ public void SetHoldings(Symbol symbol, int percentage, bool liquidateExistingHol
14011409
/// <param name="liquidateExistingHoldings">bool flag to clean all existing holdings before setting new faction.</param>
14021410
/// <param name="tag">Tag the order with a short string.</param>
14031411
/// <param name="orderProperties">The order properties to use. Defaults to <see cref="DefaultOrderProperties"/></param>
1412+
/// <returns>A list of order tickets.</returns>
14041413
/// <seealso cref="MarketOrder(QuantConnect.Symbol, decimal, bool, string, IOrderProperties)"/>
14051414
[DocumentationAttribute(TradingAndOrders)]
1406-
public void SetHoldings(Symbol symbol, decimal percentage, bool liquidateExistingHoldings = false, string tag = null, IOrderProperties orderProperties = null)
1415+
public List<OrderTicket> SetHoldings(Symbol symbol, decimal percentage, bool liquidateExistingHoldings = false, string tag = null, IOrderProperties orderProperties = null)
14071416
{
1408-
SetHoldingsImpl(symbol, CalculateOrderQuantity(symbol, percentage), liquidateExistingHoldings, tag, orderProperties);
1417+
return SetHoldingsImpl(symbol, CalculateOrderQuantity(symbol, percentage), liquidateExistingHoldings, tag, orderProperties);
14091418
}
14101419

14111420
/// <summary>
14121421
/// Set holdings implementation, which uses order quantities (delta) not percentage nor target final quantity
14131422
/// </summary>
1414-
private void SetHoldingsImpl(Symbol symbol, decimal orderQuantity, bool liquidateExistingHoldings = false, string tag = null, IOrderProperties orderProperties = null)
1423+
private List<OrderTicket> SetHoldingsImpl(Symbol symbol, decimal orderQuantity, bool liquidateExistingHoldings = false, string tag = null, IOrderProperties orderProperties = null)
14151424
{
1425+
List<OrderTicket> orderTickets = null;
14161426
//If they triggered a liquidate
14171427
if (liquidateExistingHoldings)
14181428
{
1419-
Liquidate(GetSymbolsToLiquidate([symbol]), tag: tag, orderProperties: orderProperties);
1429+
orderTickets = Liquidate(GetSymbolsToLiquidate([symbol]), tag: tag, orderProperties: orderProperties);
14201430
}
14211431

1432+
orderTickets ??= new List<OrderTicket>();
14221433
tag ??= "";
14231434
//Calculate total unfilled quantity for open market orders
14241435
var marketOrdersQuantity = Transactions.GetOpenOrderTickets(
@@ -1435,19 +1446,22 @@ private void SetHoldingsImpl(Symbol symbol, decimal orderQuantity, bool liquidat
14351446
if (!Securities.TryGetValue(symbol, out security))
14361447
{
14371448
Error($"{symbol} not found in portfolio. Request this data when initializing the algorithm.");
1438-
return;
1449+
return orderTickets;
14391450
}
14401451

14411452
//Check whether the exchange is open to send a market order. If not, send a market on open order instead
1453+
OrderTicket ticket;
14421454
if (security.Exchange.ExchangeOpen)
14431455
{
1444-
MarketOrder(symbol, quantity, false, tag, orderProperties);
1456+
ticket = MarketOrder(symbol, quantity, false, tag, orderProperties);
14451457
}
14461458
else
14471459
{
1448-
MarketOnOpenOrder(symbol, quantity, tag, orderProperties);
1460+
ticket = MarketOnOpenOrder(symbol, quantity, tag, orderProperties);
14491461
}
1462+
orderTickets.Add(ticket);
14501463
}
1464+
return orderTickets;
14511465
}
14521466

14531467
/// <summary>

0 commit comments

Comments
 (0)