Skip to content

Commit

Permalink
Add liquidate method overloads (QuantConnect#8210)
Browse files Browse the repository at this point in the history
* First draft of the solution

* First draft of the tests

* Nit changes

* Nit change

* Add improvements

* Improve unit tests

* Regression algos

* Nit change

* Nit changes

* Improve unit tests

* Improve unit test

* Nit change

* Address reviews
  • Loading branch information
Marinovsky authored Jul 22, 2024
1 parent 0352a0b commit 97959d5
Show file tree
Hide file tree
Showing 7 changed files with 361 additions and 35 deletions.
123 changes: 123 additions & 0 deletions Algorithm.CSharp/CanLiquidateWithOrderPropertiesRegressionAlgorithm.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/*
* 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 QuantConnect.Data;
using QuantConnect.Interfaces;
using QuantConnect.Orders;
using System;
using System.Collections.Generic;

namespace QuantConnect.Algorithm.CSharp
{
/// <summary>
/// Regression algorithm to test we can liquidate our portfolio holdings using order properties
/// </summary>
public class CanLiquidateWithOrderPropertiesRegressionAlgorithm: QCAlgorithm, IRegressionAlgorithmDefinition
{
private readonly DateTime _openExchange = new (2014, 6, 6, 10, 0, 0);
private readonly DateTime _closeExchange = new(2014, 6, 6, 16, 0, 0);

public override void Initialize()
{
SetStartDate(2014, 6, 5);
SetEndDate(2014, 6, 6);

AddEquity("AAPL", Resolution.Minute);
}

public override void OnData(Slice slice)
{
if (Time > _openExchange && Time < _closeExchange)
{
if (!Portfolio.Invested)
{
MarketOrder("AAPL", 10);
}
else
{
var orderProperties = new OrderProperties() { TimeInForce = TimeInForce.Day };
var tickets = Liquidate(asynchronous: true, orderProperties: orderProperties);
foreach (var ticket in tickets)
{
if (ticket.SubmitRequest.OrderProperties.TimeInForce != TimeInForce.Day)
{
throw new RegressionTestException("The TimeInForce for all orders should be daily, but it was {ticket.SubmitRequest.OrderProperties.TimeInForce}");
}
}
}
}
}

/// <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, Language.Python };

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

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

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

/// <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", "359"},
{"Average Win", "0%"},
{"Average Loss", "0%"},
{"Compounding Annual Return", "0%"},
{"Drawdown", "0%"},
{"Expectancy", "0"},
{"Start Equity", "100000"},
{"End Equity", "99637.08"},
{"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", "0"},
{"Tracking Error", "0"},
{"Treynor Ratio", "0"},
{"Total Fees", "$359.00"},
{"Estimated Strategy Capacity", "$130000000.00"},
{"Lowest Capacity Asset", "AAPL R735QTJ8XC9X"},
{"Portfolio Turnover", "37.56%"},
{"OrderListHash", "e9e8a07dc58bff7198181f9fafb58834"}
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# 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.

from AlgorithmImports import *

### <summary>
### Regression algorithm to test we can liquidate our portfolio holdings using order properties
### </summary>
class CanLiquidateWithOrderPropertiesRegressionAlgorithm(QCAlgorithm):
def initialize(self):
self.set_start_date(2014, 6, 5)
self.set_end_date(2014, 6, 6)
self.set_cash(100000)

self.open_exchange = datetime(2014, 6, 6, 10, 0, 0)
self.close_exchange = datetime(2014, 6, 6, 16, 0, 0)
self.add_equity("AAPL", resolution = Resolution.MINUTE)

def on_data(self, slice):
if self.time > self.open_exchange and self.time < self.close_exchange:
if not self.portfolio.invested:
self.market_order("AAPL", 10)
else:
order_properties = OrderProperties()
order_properties.time_in_force = TimeInForce.DAY
tickets = self.liquidate(asynchronous = True, order_properties = order_properties)
for ticket in tickets:
if ticket.SubmitRequest.OrderProperties.TimeInForce != TimeInForce.DAY:
raise Exception(f"The TimeInForce for all orders should be daily, but it was {ticket.SubmitRequest.OrderProperties.TimeInForce}")
15 changes: 15 additions & 0 deletions Algorithm/QCAlgorithm.Python.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
using QuantConnect.Brokerages;
using QuantConnect.Scheduling;
using QuantConnect.Util;
using QuantConnect.Interfaces;
using QuantConnect.Orders;

namespace QuantConnect.Algorithm
{
Expand Down Expand Up @@ -1575,6 +1577,19 @@ public IndicatorHistory IndicatorHistory(PyObject indicator, IEnumerable<Slice>
return IndicatorHistory(WrapPythonIndicator(indicator), history, selector?.ConvertToDelegate<Func<IBaseData, IBaseData>>());
}

/// <summary>
/// Liquidate your portfolio holdings
/// </summary>
/// <param name="symbols">List of symbols to liquidate in Python</param>
/// <param name="asynchronous">Flag to indicate if the symbols should be liquidated asynchronously</param>
/// <param name="tag">Custom tag to know who is calling this</param>
/// <param name="orderProperties">Order properties to use</param>
[DocumentationAttribute(TradingAndOrders)]
public List<OrderTicket> Liquidate(PyObject symbols, bool asynchronous = false, string tag = "Liquidated", IOrderProperties orderProperties = null)
{
return Liquidate(symbols.ConvertToSymbolEnumerable(), asynchronous, tag, orderProperties);
}

/// <summary>
/// Gets indicator base type
/// </summary>
Expand Down
72 changes: 47 additions & 25 deletions Algorithm/QCAlgorithm.Trading.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1198,41 +1198,53 @@ private OrderResponse PreOrderChecksImpl(SubmitOrderRequest request)
}

/// <summary>
/// Liquidate all holdings and cancel open orders. Called at the end of day for tick-strategies.
/// Liquidate your portfolio holdings
/// </summary>
/// <param name="symbolToLiquidate">Symbols we wish to liquidate</param>
/// <param name="tag">Custom tag to know who is calling this.</param>
/// <returns>Array of order ids for liquidated symbols</returns>
/// <seealso cref="MarketOrder(QuantConnect.Symbol, decimal, bool, string, IOrderProperties)"/>
/// <param name="symbol">Specific asset to liquidate, defaults to all</param>
/// <param name="asynchronous">Flag to indicate if the symbols should be liquidated asynchronously</param>
/// <param name="tag">Custom tag to know who is calling this</param>
/// <param name="orderProperties">Order properties to use</param>
[DocumentationAttribute(TradingAndOrders)]
public List<int> Liquidate(Symbol symbolToLiquidate = null, string tag = "Liquidated")
public List<OrderTicket> Liquidate(Symbol symbol = null, bool asynchronous = false, string tag = "Liquidated", IOrderProperties orderProperties = null)
{
var orderIdList = new List<int>();
if (!Settings.LiquidateEnabled)
{
Debug("Liquidate() is currently disabled by settings. To re-enable please set 'Settings.LiquidateEnabled' to true");
return orderIdList;
}

IEnumerable<Symbol> toLiquidate;
if (symbolToLiquidate != null)
if (symbol != null)
{
toLiquidate = Securities.ContainsKey(symbolToLiquidate)
? new[] { symbolToLiquidate } : Enumerable.Empty<Symbol>();
toLiquidate = Securities.ContainsKey(symbol)
? new[] { symbol } : Enumerable.Empty<Symbol>();
}
else
{
toLiquidate = Securities.Keys.OrderBy(x => x.Value);
}

return Liquidate(toLiquidate, asynchronous, tag, orderProperties);
}

foreach (var symbol in toLiquidate)
/// <summary>
/// Liquidate your portfolio holdings
/// </summary>
/// <param name="symbols">List of symbols to liquidate, defaults to all</param>
/// <param name="asynchronous">Flag to indicate if the symbols should be liquidated asynchronously</param>
/// <param name="tag">Custom tag to know who is calling this</param>
/// <param name="orderProperties">Order properties to use</param>
[DocumentationAttribute(TradingAndOrders)]
public List<OrderTicket> Liquidate(IEnumerable<Symbol> symbols, bool asynchronous = false, string tag = "Liquidated", IOrderProperties orderProperties = null)
{
var orderTickets = new List<OrderTicket>();
if (!Settings.LiquidateEnabled)
{
Debug("Liquidate() is currently disabled by settings. To re-enable please set 'Settings.LiquidateEnabled' to true");
return orderTickets;
}

foreach (var symbolToLiquidate in symbols)
{
// get open orders
var orders = Transactions.GetOpenOrders(symbol);
var orders = Transactions.GetOpenOrders(symbolToLiquidate);

// get quantity in portfolio
var quantity = Portfolio[symbol].Quantity;
var quantity = Portfolio[symbolToLiquidate].Quantity;

// if there is only one open market order that would close the position, do nothing
if (orders.Count == 1 && quantity != 0 && orders[0].Quantity == -quantity && orders[0].Type == OrderType.Market)
Expand Down Expand Up @@ -1262,15 +1274,25 @@ public List<int> Liquidate(Symbol symbolToLiquidate = null, string tag = "Liquid
if (quantity != 0)
{
// calculate quantity for closing market order
var ticket = Order(symbol, -quantity - marketOrdersQuantity, tag: tag);
if (ticket.Status == OrderStatus.Filled)
{
orderIdList.Add(ticket.OrderId);
}
var ticket = Order(symbolToLiquidate, -quantity - marketOrdersQuantity, asynchronous: asynchronous, tag: tag, orderProperties: orderProperties);
orderTickets.Add(ticket);
}
}

return orderIdList;
return orderTickets;
}

/// <summary>
/// Liquidate all holdings and cancel open orders. Called at the end of day for tick-strategies.
/// </summary>
/// <param name="symbolToLiquidate">Symbol we wish to liquidate</param>
/// <param name="tag">Custom tag to know who is calling this.</param>
/// <returns>Array of order ids for liquidated symbols</returns>
/// <seealso cref="MarketOrder(QuantConnect.Symbol, decimal, bool, string, IOrderProperties)"/>
[Obsolete($"This method is obsolete, please use Liquidate(symbol: symbolToLiquidate, tag: tag) method")]
public List<int> Liquidate(Symbol symbolToLiquidate, string tag)
{
return Liquidate(symbol: symbolToLiquidate, tag:tag).Select(x => x.OrderId).ToList();
}

/// <summary>
Expand Down
11 changes: 6 additions & 5 deletions AlgorithmFactory/Python/Wrappers/AlgorithmPythonWrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -709,12 +709,13 @@ public void Initialize()
}

/// <summary>
/// Liquidate your portfolio holdings:
/// Liquidate your portfolio holdings
/// </summary>
/// <param name="symbolToLiquidate">Specific asset to liquidate, defaults to all.</param>
/// <param name="tag">Custom tag to know who is calling this.</param>
/// <returns>list of order ids</returns>
public List<int> Liquidate(Symbol symbolToLiquidate = null, string tag = "Liquidated") => _baseAlgorithm.Liquidate(symbolToLiquidate, tag);
/// <param name="symbol">Specific asset to liquidate, defaults to all</param>
/// <param name="asynchronous">Flag to indicate if the symbols should be liquidated asynchronously</param>
/// <param name="tag">Custom tag to know who is calling this</param>
/// <param name="orderProperties">Order properties to use</param>
public List<OrderTicket> Liquidate(Symbol symbol = null, bool asynchronous = false, string tag = "Liquidated", IOrderProperties orderProperties = null) => _baseAlgorithm.Liquidate(symbol, asynchronous, tag, orderProperties);

/// <summary>
/// Save entry to the Log
Expand Down
11 changes: 6 additions & 5 deletions Common/Interfaces/IAlgorithm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -772,12 +772,13 @@ Security AddSecurity(Symbol symbol, Resolution? resolution = null, bool fillForw
void SetCash(string symbol, decimal startingCash, decimal conversionRate = 0);

/// <summary>
/// Liquidate your portfolio holdings:
/// Liquidate your portfolio holdings
/// </summary>
/// <param name="symbolToLiquidate">Specific asset to liquidate, defaults to all.</param>
/// <param name="tag">Custom tag to know who is calling this.</param>
/// <returns>list of order ids</returns>
List<int> Liquidate(Symbol symbolToLiquidate = null, string tag = "Liquidated");
/// <param name="symbol">Specific asset to liquidate, defaults to all.</param>
/// <param name="asynchronous">Flag to indicate if the symbols should be liquidated asynchronously</param>
/// <param name="tag">Custom tag to know who is calling this</param>
/// <param name="orderProperties">Order properties to use</param>
List<OrderTicket> Liquidate(Symbol symbol = null, bool asynchronous = false, string tag = "Liquidated", IOrderProperties orderProperties = null);

/// <summary>
/// Set live mode state of the algorithm run: Public setter for the algorithm property LiveMode.
Expand Down
Loading

0 comments on commit 97959d5

Please sign in to comment.