Skip to content

Commit

Permalink
Delayed Settlement Cash Not Freed for Removed Assets (#7727)
Browse files Browse the repository at this point in the history
* Delay security removal when there are pending settlements

* Add unit test

* Minor fixes

* Minor fix

* Replace HasUnsettledFunds method with UnsettledCash in ISettlementModel

* Minor fix

* Minor fix

* Address peer review

Replace ISettlementModel UnsettledCash property with GetUnsettledCash method that returns a CashAmount instead of a decimal to include the currency

* Minor fix

* Address peer review
  • Loading branch information
jhonabreul authored Feb 5, 2024
1 parent ef728bc commit 98a3fd4
Show file tree
Hide file tree
Showing 13 changed files with 338 additions and 34 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/*
* 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.Interfaces;
using System.Linq;
using System;
using QuantConnect.Securities;
using QuantConnect.Securities.Option;

namespace QuantConnect.Algorithm.CSharp
{
/// <summary>
/// Algorithm asserting that delayed cash settlement is applied even when the option contract is manually removed
/// </summary>
public class DelayedSettlementAfterManualSecurityRemovalAlgorithm : QCAlgorithm, IRegressionAlgorithmDefinition
{
private Symbol _optionSymbol;

public override void Initialize()
{
SetStartDate(2015, 12, 24);
SetEndDate(2015, 12, 31);
SetCash(100000);

var equity = AddEquity("GOOG");

_optionSymbol = OptionChainProvider.GetOptionContractList(equity.Symbol, Time)
.OrderByDescending(symbol => symbol.ID.Date)
.First(optionContract => optionContract.ID.OptionRight == OptionRight.Call);
var option = AddOptionContract(_optionSymbol);

option.SetSettlementModel(new DelayedSettlementModel(Option.DefaultSettlementDays, Option.DefaultSettlementTime));

Schedule.On(DateRules.On(StartDate), TimeRules.BeforeMarketClose(_optionSymbol, 30), () =>
{
MarketOrder(_optionSymbol, 1);
});

Schedule.On(DateRules.On(StartDate), TimeRules.BeforeMarketClose(_optionSymbol, 1), () =>
{
RemoveOptionContract(_optionSymbol);
});

var expectedSettlementDate = new DateTime(2015, 12, 28);

Schedule.On(DateRules.On(expectedSettlementDate), TimeRules.AfterMarketOpen(_optionSymbol), () =>
{
if (Portfolio.UnsettledCash == 0)
{
throw new Exception($"Expected unsettled cash to be non-zero at {Time}");
}
});

Schedule.On(DateRules.On(expectedSettlementDate), TimeRules.BeforeMarketClose(_optionSymbol), () =>
{
if (Portfolio.UnsettledCash != 0)
{
throw new Exception($"Expected unsettled cash to be zero at {Time}");
}
});
}

public override void OnEndOfAlgorithm()
{
if (Transactions.OrdersCount != 2)
{
throw new Exception($"Expected 2 orders, found {Transactions.OrdersCount}");
}

if (Portfolio.Invested)
{
throw new Exception("Expected no holdings at end of algorithm");
}

if (Portfolio.UnsettledCash != 0)
{
throw new Exception($"Expected no unsettled cash at end of algorithm, found {Portfolio.UnsettledCash}");
}
}

/// <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 Language[] Languages { get; } = { Language.CSharp };

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

/// <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 Trades", "2"},
{"Average Win", "0%"},
{"Average Loss", "-0.36%"},
{"Compounding Annual Return", "-15.857%"},
{"Drawdown", "0.400%"},
{"Expectancy", "-1"},
{"Net Profit", "-0.362%"},
{"Sharpe Ratio", "0"},
{"Sortino Ratio", "0"},
{"Probabilistic Sharpe Ratio", "0%"},
{"Loss Rate", "100%"},
{"Win Rate", "0%"},
{"Profit-Loss Ratio", "0"},
{"Alpha", "0"},
{"Beta", "0"},
{"Annual Standard Deviation", "0"},
{"Annual Variance", "0"},
{"Information Ratio", "2.537"},
{"Tracking Error", "0.104"},
{"Treynor Ratio", "0"},
{"Total Fees", "$2.00"},
{"Estimated Strategy Capacity", "$150000.00"},
{"Lowest Capacity Asset", "GOOCV WRCOZDXBITL2|GOOCV VP83T1ZUHROL"},
{"Portfolio Turnover", "1.06%"},
{"OrderListHash", "490dd9430dec6cb3daadcca495ff5f12"}
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@

using QuantConnect.Data;
using QuantConnect.Securities;
using QuantConnect.Brokerages;
using System;
using QuantConnect.Interfaces;
using System.Collections.Generic;
Expand Down Expand Up @@ -133,5 +132,13 @@ public void Scan(ScanSettlementModelParameters settlementParameters)
settlementParameters.Portfolio.CashBook[_currency].AddAmount(-_amount);
}
}

/// <summary>
/// Gets the unsettled cash amount for the security
/// </summary>
public CashAmount GetUnsettledCash()
{
return default;
}
}
}
3 changes: 3 additions & 0 deletions Algorithm.Python/CustomSettlementModelRegressionAlgorithm.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ def Scan(self, parameters):
if parameters.UtcTime == datetime(2013, 10, 6):
parameters.Portfolio.CashBook[self.currency].AddAmount(-self.amount)

def GetUnsettledCash(self):
return None

class CustomBrokerageModelWithCustomSettlementModel(CustomBrokerageModel):
def GetSettlementModel(self, security):
return CustomSettlementModel()
19 changes: 18 additions & 1 deletion Common/Python/SettlementModelPythonWrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public class SettlementModelPythonWrapper : ISettlementModel
/// <param name="model">Settlement Python Model</param>
public SettlementModelPythonWrapper(PyObject model)
{
_model = model;
_model = model.ValidateImplementationOf<ISettlementModel>();
}

/// <summary>
Expand All @@ -57,5 +57,22 @@ public void Scan(ScanSettlementModelParameters settlementParameters)
_model.Scan(settlementParameters);
}
}

/// <summary>
/// Gets the unsettled cash amount for the security
/// </summary>
public CashAmount GetUnsettledCash()
{
using (Py.GIL())
{
var result = _model.GetUnsettledCash();
if (result == null)
{
return default;
}

return result;
}
}
}
}
12 changes: 2 additions & 10 deletions Common/Securities/AccountCurrencyImmediateSettlementModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@ namespace QuantConnect.Securities
/// Represents the model responsible for applying cash settlement rules
/// </summary>
/// <remarks>This model converts the amount to the account currency and applies cash settlement immediately</remarks>
public class AccountCurrencyImmediateSettlementModel : ISettlementModel
public class AccountCurrencyImmediateSettlementModel : ImmediateSettlementModel
{
/// <summary>
/// Applies cash settlement rules
/// </summary>
/// <param name="applyFundsParameters">The funds application parameters</param>
public void ApplyFunds(ApplyFundsSettlementModelParameters applyFundsParameters)
public override void ApplyFunds(ApplyFundsSettlementModelParameters applyFundsParameters)
{
var currency = applyFundsParameters.CashAmount.Currency;
var amount = applyFundsParameters.CashAmount.Amount;
Expand All @@ -34,13 +34,5 @@ public void ApplyFunds(ApplyFundsSettlementModelParameters applyFundsParameters)

portfolio.CashBook[portfolio.CashBook.AccountCurrency].AddAmount(amountInAccountCurrency);
}

/// <summary>
/// Scan for pending settlements
/// </summary>
/// <param name="settlementParameters">The settlement parameters</param>
public void Scan(ScanSettlementModelParameters settlementParameters)
{
}
}
}
2 changes: 1 addition & 1 deletion Common/Securities/CashAmount.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/*
/*
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
*
Expand Down
33 changes: 30 additions & 3 deletions Common/Securities/DelayedSettlementModel.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
/*
* 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");
*
* 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.
Expand All @@ -15,6 +15,7 @@

using System;
using System.Collections.Generic;
using System.Linq;

namespace QuantConnect.Securities
{
Expand All @@ -26,6 +27,7 @@ public class DelayedSettlementModel : ISettlementModel
{
private readonly int _numberOfDays;
private readonly TimeSpan _timeOfDay;
private CashBook _cashBook;

/// <summary>
/// The list of pending funds waiting for settlement time
Expand Down Expand Up @@ -85,6 +87,12 @@ public void ApplyFunds(ApplyFundsSettlementModelParameters applyFundsParameters)

portfolio.CashBook[currency].AddAmount(amount);
}

// We just keep it to use currency conversion in GetUnsettledCash method
if (_cashBook == null)
{
_cashBook = portfolio.UnsettledCashBook;
}
}

/// <summary>
Expand All @@ -110,5 +118,24 @@ public void Scan(ScanSettlementModelParameters settlementParameters)
}
}
}

/// <summary>
/// Gets the unsettled cash amount for the security
/// </summary>
public CashAmount GetUnsettledCash()
{
var accountCurrency = _cashBook != null ? _cashBook.AccountCurrency : Currencies.USD;

lock (_unsettledCashAmounts)
{
if (_unsettledCashAmounts.Count == 0)
{
return default;
}

return new CashAmount(_unsettledCashAmounts.Sum(x => _cashBook.ConvertToAccountCurrency(x.Amount, x.Currency)), accountCurrency);
}

}
}
}
11 changes: 8 additions & 3 deletions Common/Securities/ISettlementModel.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
/*
* 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");
*
* 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.
Expand All @@ -31,5 +31,10 @@ public interface ISettlementModel
/// </summary>
/// <param name="settlementParameters">The settlement parameters</param>
void Scan(ScanSettlementModelParameters settlementParameters);

/// <summary>
/// Gets the unsettled cash amount for the security
/// </summary>
CashAmount GetUnsettledCash();
}
}
14 changes: 11 additions & 3 deletions Common/Securities/ImmediateSettlementModel.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
/*
* 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");
*
* 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.
Expand Down Expand Up @@ -39,5 +39,13 @@ public virtual void ApplyFunds(ApplyFundsSettlementModelParameters applyFundsPar
public virtual void Scan(ScanSettlementModelParameters settlementParameters)
{
}

/// <summary>
/// Gets the unsettled cash amount for the security
/// </summary>
public virtual CashAmount GetUnsettledCash()
{
return default;
}
}
}
10 changes: 9 additions & 1 deletion Engine/DataFeeds/PendingRemovalsManager.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/*
/*
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
*
Expand Down Expand Up @@ -61,6 +61,14 @@ private bool IsSafeToRemove(Security member, Universe universe)
// covers the options use case
return false;
}

// don't remove if there are unsettled positions
var unsettledCash = member.SettlementModel.GetUnsettledCash();
if (unsettledCash != default && unsettledCash.Amount > 0)
{
return false;
}

return true;
}

Expand Down
Loading

0 comments on commit 98a3fd4

Please sign in to comment.