Skip to content

Commit

Permalink
Refactor SetHoldings to return a List<OrderTicket> (#8550)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
JosueNina authored Jan 23, 2025
1 parent c5c56bf commit cad5f20
Show file tree
Hide file tree
Showing 6 changed files with 224 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public override void Rebalance()

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

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

if (nonCanceledOrdersCount != OrderTickets.Count)
{
throw new RegressionTestException($"Expected {OrderTickets.Count} non-canceled orders, but found {nonCanceledOrdersCount}.");
}

// Verify all tags are "LiquidatedTest"
var invalidTags = orders.Where(order => order.Tag != "LiquidatedTest").ToList();
if (invalidTags.Count != 0)
Expand Down
7 changes: 7 additions & 0 deletions Algorithm.CSharp/LiquidateRegressionAlgorithm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ namespace QuantConnect.Algorithm.CSharp
/// </summary>
public class LiquidateRegressionAlgorithm : QCAlgorithm, IRegressionAlgorithmDefinition
{
protected List<OrderTicket> OrderTickets { get; private set; }
protected Symbol Spy { get; private set; }
protected Symbol Ibm { get; private set; }
public override void Initialize()
Expand All @@ -35,6 +36,7 @@ public override void Initialize()
SetEndDate(2018, 1, 10);
Spy = AddEquity("SPY", Resolution.Daily).Symbol;
Ibm = AddEquity("IBM", Resolution.Daily).Symbol;
OrderTickets = new List<OrderTicket>();

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

if (OrderTickets.Count > 0)
{
throw new RegressionTestException("The number of order tickets must be zero because all orders were cancelled");
}

// Check if there are any holdings left in the portfolio
foreach (var kvp in Portfolio)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public class LiquidateUsingSetHoldingsRegressionAlgorithm : LiquidateRegressionA
public override void PerformLiquidation()
{
var properties = new OrderProperties { TimeInForce = TimeInForce.GoodTilCanceled };
SetHoldings(new List<PortfolioTarget>(), true, "LiquidatedTest", properties);
OrderTickets.AddRange(SetHoldings(new List<PortfolioTarget>(), true, "LiquidatedTest", properties));
var orders = Transactions.GetOrders().ToList();
var orderTags = orders.Where(e => e.Tag == "LiquidatedTest").ToList();
if (orderTags.Count != orders.Count)
Expand Down
116 changes: 116 additions & 0 deletions Algorithm.CSharp/SetHoldingReturnsOrderTicketsRegressionAlgorithm.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/*
* 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 QuantConnect.Algorithm.Framework.Portfolio;
using QuantConnect.Data;
using QuantConnect.Interfaces;

namespace QuantConnect.Algorithm.CSharp
{
/// <summary>
/// Validates that SetHoldings returns the correct number of order tickets on each execution.
/// </summary>
public class SetHoldingReturnsOrderTicketsRegressionAlgorithm : QCAlgorithm, IRegressionAlgorithmDefinition
{
private Symbol _spy;
private Symbol _ibm;
public override void Initialize()
{
SetStartDate(2018, 1, 4);
SetEndDate(2018, 1, 10);
_spy = AddEquity("SPY", Resolution.Daily).Symbol;
_ibm = AddEquity("IBM", Resolution.Daily).Symbol;
}

public override void OnData(Slice slice)
{
var tickets = SetHoldings(new List<PortfolioTarget> { new(_spy, 0.8m), new(_ibm, 0.2m) });

if (!Portfolio.Invested)
{
// Ensure exactly 2 tickets are created when the portfolio is not yet invested
if (tickets.Count != 2)
{
throw new RegressionTestException("Expected 2 tickets, got " + tickets.Count);
}
}
else if (tickets.Count != 0)
{
// Ensure no tickets are created when the portfolio is already invested
throw new RegressionTestException("Expected 0 tickets, got " + tickets.Count);
}
}

/// <summary>
/// Final status of the algorithm
/// </summary>
public AlgorithmStatus AlgorithmStatus => AlgorithmStatus.Completed;

/// <summary>
/// This is used by the regression test system to indicate if the open source Lean repository has the required data to run this algorithm.
/// </summary>
public bool CanRunLocally { get; } = true;

/// <summary>
/// This is used by the regression test system to indicate which languages this algorithm is written in.
/// </summary>
public List<Language> Languages { get; } = new() { Language.CSharp };

/// <summary>
/// Data Points count of all timeslices of algorithm
/// </summary>
public long DataPoints => 53;

/// <summary>
/// Data Points count of the algorithm history
/// </summary>
public int AlgorithmHistoryDataPoints => 0;

/// <summary>
/// This is used by the regression test system to indicate what the expected statistics are from running the algorithm
/// </summary>
public Dictionary<string, string> ExpectedStatistics => new Dictionary<string, string>
{
{"Total Orders", "2"},
{"Average Win", "0%"},
{"Average Loss", "0%"},
{"Compounding Annual Return", "43.490%"},
{"Drawdown", "0.100%"},
{"Expectancy", "0"},
{"Start Equity", "100000"},
{"End Equity", "100661.71"},
{"Net Profit", "0.662%"},
{"Sharpe Ratio", "12.329"},
{"Sortino Ratio", "0"},
{"Probabilistic Sharpe Ratio", "97.100%"},
{"Loss Rate", "0%"},
{"Win Rate", "0%"},
{"Profit-Loss Ratio", "0"},
{"Alpha", "0.108"},
{"Beta", "0.424"},
{"Annual Standard Deviation", "0.024"},
{"Annual Variance", "0.001"},
{"Information Ratio", "-5.097"},
{"Tracking Error", "0.03"},
{"Treynor Ratio", "0.707"},
{"Total Fees", "$2.56"},
{"Estimated Strategy Capacity", "$170000000.00"},
{"Lowest Capacity Asset", "IBM R735QTJ8XC9X"},
{"Portfolio Turnover", "14.24%"},
{"OrderListHash", "587e1a69d3c83cbd9907f9f9586697e1"}
};
}
}
46 changes: 30 additions & 16 deletions Algorithm/QCAlgorithm.Trading.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1326,23 +1326,28 @@ public void SetMaximumOrders(int max)
/// <param name="liquidateExistingHoldings">True will liquidate existing holdings</param>
/// <param name="tag">Tag the order with a short string.</param>
/// <param name="orderProperties">The order properties to use. Defaults to <see cref="DefaultOrderProperties"/></param>
/// <returns>A list of order tickets.</returns>
/// <seealso cref="MarketOrder(QuantConnect.Symbol, decimal, bool, string, IOrderProperties)"/>
[DocumentationAttribute(TradingAndOrders)]
public void SetHoldings(List<PortfolioTarget> targets, bool liquidateExistingHoldings = false, string tag = null, IOrderProperties orderProperties = null)
public List<OrderTicket> SetHoldings(List<PortfolioTarget> targets, bool liquidateExistingHoldings = false, string tag = null, IOrderProperties orderProperties = null)
{
List<OrderTicket> orderTickets = null;
//If they triggered a liquidate
if (liquidateExistingHoldings)
{
Liquidate(GetSymbolsToLiquidate(targets.Select(t => t.Symbol)), tag: tag, orderProperties: orderProperties);
orderTickets = Liquidate(GetSymbolsToLiquidate(targets.Select(t => t.Symbol)), tag: tag, orderProperties: orderProperties);
}
orderTickets ??= new List<OrderTicket>();

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))
{
SetHoldingsImpl(portfolioTarget.Symbol, portfolioTarget.Quantity, false, tag, orderProperties);
var tickets = SetHoldingsImpl(portfolioTarget.Symbol, portfolioTarget.Quantity, false, tag, orderProperties);
orderTickets.AddRange(tickets);
}
return orderTickets;
}

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

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

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

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

/// <summary>
/// Set holdings implementation, which uses order quantities (delta) not percentage nor target final quantity
/// </summary>
private void SetHoldingsImpl(Symbol symbol, decimal orderQuantity, bool liquidateExistingHoldings = false, string tag = null, IOrderProperties orderProperties = null)
private List<OrderTicket> SetHoldingsImpl(Symbol symbol, decimal orderQuantity, bool liquidateExistingHoldings = false, string tag = null, IOrderProperties orderProperties = null)
{
List<OrderTicket> orderTickets = null;
//If they triggered a liquidate
if (liquidateExistingHoldings)
{
Liquidate(GetSymbolsToLiquidate([symbol]), tag: tag, orderProperties: orderProperties);
orderTickets = Liquidate(GetSymbolsToLiquidate([symbol]), tag: tag, orderProperties: orderProperties);
}

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

//Check whether the exchange is open to send a market order. If not, send a market on open order instead
OrderTicket ticket;
if (security.Exchange.ExchangeOpen)
{
MarketOrder(symbol, quantity, false, tag, orderProperties);
ticket = MarketOrder(symbol, quantity, false, tag, orderProperties);
}
else
{
MarketOnOpenOrder(symbol, quantity, tag, orderProperties);
ticket = MarketOnOpenOrder(symbol, quantity, tag, orderProperties);
}
orderTickets.Add(ticket);
}
return orderTickets;
}

/// <summary>
Expand Down
Loading

0 comments on commit cad5f20

Please sign in to comment.