Skip to content

Commit

Permalink
Fix index option cash profit (#8520)
Browse files Browse the repository at this point in the history
- Fix so that option cash settlement impacts profit records. Updating
  existing and adding new tests
  • Loading branch information
Martin-Molinero authored Jan 13, 2025
1 parent 50f887c commit 6982f67
Show file tree
Hide file tree
Showing 20 changed files with 125 additions and 100 deletions.
12 changes: 6 additions & 6 deletions Algorithm.CSharp/BasicTemplateSPXWeeklyIndexOptionsAlgorithm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -121,20 +121,20 @@ public override void OnOrderEvent(OrderEvent orderEvent)
public virtual Dictionary<string, string> ExpectedStatistics => new Dictionary<string, string>
{
{"Total Orders", "5"},
{"Average Win", "0%"},
{"Average Loss", "-0.69%"},
{"Average Win", "0.63%"},
{"Average Loss", "-0.03%"},
{"Compounding Annual Return", "54.478%"},
{"Drawdown", "0.400%"},
{"Expectancy", "-0.5"},
{"Expectancy", "23.219"},
{"Start Equity", "1000000"},
{"End Equity", "1006025"},
{"Net Profit", "0.602%"},
{"Sharpe Ratio", "2.62"},
{"Sortino Ratio", "0"},
{"Probabilistic Sharpe Ratio", "63.221%"},
{"Loss Rate", "50%"},
{"Win Rate", "50%"},
{"Profit-Loss Ratio", "0"},
{"Loss Rate", "0%"},
{"Win Rate", "100%"},
{"Profit-Loss Ratio", "23.22"},
{"Alpha", "0.067"},
{"Beta", "-0.013"},
{"Annual Standard Deviation", "0.004"},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,11 @@ public override void OnOrderEvent(OrderEvent orderEvent)
public virtual Dictionary<string, string> ExpectedStatistics => new Dictionary<string, string>
{
{"Total Orders", "10"},
{"Average Win", "0.47%"},
{"Average Win", "0.46%"},
{"Average Loss", "-0.01%"},
{"Compounding Annual Return", "101.998%"},
{"Drawdown", "0.100%"},
{"Expectancy", "24.484"},
{"Expectancy", "24.137"},
{"Start Equity", "1000000"},
{"End Equity", "1009050"},
{"Net Profit", "0.905%"},
Expand All @@ -140,7 +140,7 @@ public override void OnOrderEvent(OrderEvent orderEvent)
{"Probabilistic Sharpe Ratio", "95.546%"},
{"Loss Rate", "50%"},
{"Win Rate", "50%"},
{"Profit-Loss Ratio", "49.97"},
{"Profit-Loss Ratio", "49.27"},
{"Alpha", "-2.01"},
{"Beta", "0.307"},
{"Annual Standard Deviation", "0.021"},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,16 @@ public override void OnEndOfAlgorithm()
{
throw new RegressionTestException("Expected to try to exercise option before and on expiry");
}

var optionHoldings = Securities[_contract.Symbol].Holdings;
if (optionHoldings.NetProfit != Portfolio.TotalNetProfit)
{
throw new RegressionTestException($"Unexpected holdings profit result {optionHoldings.Profit}");
}
if (Portfolio.Cash != (Portfolio.TotalNetProfit + 200000))
{
throw new RegressionTestException($"Unexpected portfolio cash {Portfolio.Cash}");
}
}

/// <summary>
Expand Down Expand Up @@ -150,8 +160,8 @@ public override void OnEndOfAlgorithm()
public Dictionary<string, string> ExpectedStatistics => new Dictionary<string, string>
{
{"Total Orders", "2"},
{"Average Win", "0%"},
{"Average Loss", "-4.10%"},
{"Average Win", "0.68%"},
{"Average Loss", "0%"},
{"Compounding Annual Return", "24.075%"},
{"Drawdown", "1.900%"},
{"Expectancy", "0"},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ public override void Initialize()
public override Dictionary<string, string> ExpectedStatistics => new Dictionary<string, string>
{
{"Total Orders", "2"},
{"Average Win", "0%"},
{"Average Loss", "-49.28%"},
{"Average Win", "10.27%"},
{"Average Loss", "0%"},
{"Compounding Annual Return", "301.565%"},
{"Drawdown", "0.300%"},
{"Expectancy", "0"},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,8 +188,8 @@ public override void OnEndOfAlgorithm()
public virtual Dictionary<string, string> ExpectedStatistics => new Dictionary<string, string>
{
{"Total Orders", "2"},
{"Average Win", "0%"},
{"Average Loss", "-50.48%"},
{"Average Win", "9.07%"},
{"Average Loss", "0%"},
{"Compounding Annual Return", "243.722%"},
{"Drawdown", "2.500%"},
{"Expectancy", "0"},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,8 +174,8 @@ public override void OnEndOfAlgorithm()
public Dictionary<string, string> ExpectedStatistics => new Dictionary<string, string>
{
{"Total Orders", "2"},
{"Average Win", "0%"},
{"Average Loss", "-54.58%"},
{"Average Win", "4.97%"},
{"Average Loss", "0%"},
{"Compounding Annual Return", "99.378%"},
{"Drawdown", "7.600%"},
{"Expectancy", "0"},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,18 +204,18 @@ public override void OnEndOfAlgorithm()
{
{"Total Orders", "2"},
{"Average Win", "0%"},
{"Average Loss", "-50.30%"},
{"Average Loss", "-9.85%"},
{"Compounding Annual Return", "-77.114%"},
{"Drawdown", "12.500%"},
{"Expectancy", "-1"},
{"Expectancy", "0"},
{"Start Equity", "100000"},
{"End Equity", "90146"},
{"Net Profit", "-9.854%"},
{"Sharpe Ratio", "-1.957"},
{"Sortino Ratio", "-0.569"},
{"Probabilistic Sharpe Ratio", "0.709%"},
{"Loss Rate", "100%"},
{"Win Rate", "0%"},
{"Loss Rate", "0%"},
{"Win Rate", "100%"},
{"Profit-Loss Ratio", "0"},
{"Alpha", "-0.64"},
{"Beta", "0.196"},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,11 +110,11 @@ public override void OnEndOfAlgorithm()
public Dictionary<string, string> ExpectedStatistics => new Dictionary<string, string>
{
{"Total Orders", "4"},
{"Average Win", "0%"},
{"Average Loss", "-20.04%"},
{"Average Win", "174.10%"},
{"Average Loss", "-0.03%"},
{"Compounding Annual Return", "79228162514264337593543950335%"},
{"Drawdown", "2.100%"},
{"Expectancy", "-0.5"},
{"Expectancy", "2901.176"},
{"Start Equity", "100000"},
{"End Equity", "274018.3"},
{"Net Profit", "174.018%"},
Expand All @@ -123,7 +123,7 @@ public override void OnEndOfAlgorithm()
{"Probabilistic Sharpe Ratio", "95.428%"},
{"Loss Rate", "50%"},
{"Win Rate", "50%"},
{"Profit-Loss Ratio", "0"},
{"Profit-Loss Ratio", "5803.35"},
{"Alpha", "7.922816251426434E+28"},
{"Beta", "4.566"},
{"Annual Standard Deviation", "11.741"},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,8 +201,8 @@ public override void OnEndOfAlgorithm()
public Dictionary<string, string> ExpectedStatistics => new Dictionary<string, string>
{
{"Total Orders", "2"},
{"Average Win", "5.00%"},
{"Average Loss", "0%"},
{"Average Win", "0%"},
{"Average Loss", "-0.95%"},
{"Compounding Annual Return", "-12.719%"},
{"Drawdown", "1.200%"},
{"Expectancy", "-1"},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,19 +204,19 @@ public override void OnEndOfAlgorithm()
public Dictionary<string, string> ExpectedStatistics => new Dictionary<string, string>
{
{"Total Orders", "2"},
{"Average Win", "4.98%"},
{"Average Win", "0.94%"},
{"Average Loss", "0%"},
{"Compounding Annual Return", "14.183%"},
{"Drawdown", "0.300%"},
{"Expectancy", "0"},
{"Expectancy", "-1"},
{"Start Equity", "1000000"},
{"End Equity", "1009374"},
{"Net Profit", "0.937%"},
{"Sharpe Ratio", "2.997"},
{"Sortino Ratio", "34.286"},
{"Probabilistic Sharpe Ratio", "86.840%"},
{"Loss Rate", "0%"},
{"Win Rate", "100%"},
{"Loss Rate", "100%"},
{"Win Rate", "0%"},
{"Profit-Loss Ratio", "0"},
{"Alpha", "0.098"},
{"Beta", "-0.021"},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,8 +163,8 @@ public override void OnEndOfAlgorithm()
public Dictionary<string, string> ExpectedStatistics => new Dictionary<string, string>
{
{"Total Orders", "3"},
{"Average Win", "0%"},
{"Average Loss", "-4.10%"},
{"Average Win", "0.68%"},
{"Average Loss", "0%"},
{"Compounding Annual Return", "10.046%"},
{"Drawdown", "1.900%"},
{"Expectancy", "0"},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,8 @@ public override void OnEndOfAlgorithm()
public Dictionary<string, string> ExpectedStatistics => new Dictionary<string, string>
{
{"Total Orders", "4"},
{"Average Win", "0%"},
{"Average Loss", "-0.16%"},
{"Average Win", "0.58%"},
{"Average Loss", "0%"},
{"Compounding Annual Return", "31.165%"},
{"Drawdown", "0.300%"},
{"Expectancy", "0"},
Expand Down
2 changes: 1 addition & 1 deletion Algorithm.CSharp/StatisticsResultsAlgorithm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -229,8 +229,8 @@ private void CheckMostTradedSecurityStatistic(Dictionary<string, string> statist
{"Estimated Strategy Capacity", "$1100000.00"},
{"Lowest Capacity Asset", "IBM R735QTJ8XC9X"},
{"Portfolio Turnover", "549.26%"},
{"Most Traded Security Trade Count", "63"},
{"Most Traded Security", "IBM"},
{"Most Traded Security Trade Count", "63"},
{"OrderListHash", "8dd77e35338a81410a5b68dc8345f402"}
};
}
Expand Down
8 changes: 8 additions & 0 deletions Common/Securities/ConvertibleCashAmount.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,13 @@ public static implicit operator decimal(ConvertibleCashAmount convertibleCashAmo
{
return convertibleCashAmount.InAccountCurrency;
}

/// <summary>
/// The amount in account currency
/// </summary>
public static implicit operator CashAmount(ConvertibleCashAmount convertibleCashAmount)
{
return new CashAmount(convertibleCashAmount.Amount, convertibleCashAmount.Cash.Symbol);
}
}
}
59 changes: 20 additions & 39 deletions Common/Securities/Option/OptionPortfolioModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,7 @@
* limitations under the License.
*/

using QuantConnect.Logging;
using QuantConnect.Orders;
using static QuantConnect.StringExtensions;

namespace QuantConnect.Securities.Option
{
Expand All @@ -33,16 +31,9 @@ public class OptionPortfolioModel : SecurityPortfolioModel
/// <param name="fill">The order event fill object to be applied</param>
public override void ProcessFill(SecurityPortfolioManager portfolio, Security security, OrderEvent fill)
{
var order = portfolio.Transactions.GetOrderById(fill.OrderId);
if (order == null)
if (fill.Ticket.OrderType == OrderType.OptionExercise)
{
Log.Error(Invariant($"OptionPortfolioModel.ProcessFill(): Unable to locate Order with id {fill.OrderId}"));
return;
}

if (order.Type == OrderType.OptionExercise)
{
ProcessExerciseFill(portfolio, security, order, fill);
base.ProcessFill(portfolio, portfolio.Securities[fill.Symbol], fill);
}
else
{
Expand All @@ -52,40 +43,30 @@ public override void ProcessFill(SecurityPortfolioManager portfolio, Security se
}

/// <summary>
/// Processes exercise/assignment event to the portfolio
/// Helper method to determine the close trade profit
/// </summary>
/// <param name="portfolio">The algorithm's portfolio</param>
/// <param name="security">Option security</param>
/// <param name="order">The order object to be applied</param>
/// <param name="fill">The order event fill object to be applied</param>
public void ProcessExerciseFill(SecurityPortfolioManager portfolio, Security security, Order order, OrderEvent fill)
/// <remarks>For SettlementType.Cash we apply funds and add in the result to the profit</remarks>
protected override ConvertibleCashAmount ProcessCloseTradeProfit(SecurityPortfolioManager portfolio, Security security, OrderEvent fill)
{
var exerciseOrder = (OptionExerciseOrder)order;
var option = (Option)portfolio.Securities[exerciseOrder.Symbol];
var underlying = option.Underlying;
var cashQuote = option.QuoteCurrency;
var optionQuantity = order.Quantity;
var processSecurity = portfolio.Securities[fill.Symbol];
var baseResult = base.ProcessCloseTradeProfit(portfolio, security, fill);

// depending on option settlement terms we either add underlying to the account or add cash equivalent
// we then remove the exercised contracts from our option position
switch (option.ExerciseSettlement)
var ticket = fill.Ticket;
if (ticket.OrderType == OrderType.OptionExercise && security.Symbol.SecurityType.IsOption())
{
case SettlementType.PhysicalDelivery:

base.ProcessFill(portfolio, processSecurity, fill);
break;

case SettlementType.Cash:

var option = (Option)security;
if (option.ExerciseSettlement == SettlementType.Cash)
{
var underlying = option.Underlying;
var optionQuantity = fill.Ticket.Quantity;
var cashQuantity = -option.GetIntrinsicValue(underlying.Close) * option.ContractUnitOfTrade * optionQuantity;

// we add cash equivalent to portfolio
option.SettlementModel.ApplyFunds(new ApplyFundsSettlementModelParameters(portfolio, option, fill.UtcTime, new CashAmount(cashQuantity, cashQuote.Symbol), fill));

base.ProcessFill(portfolio, processSecurity, fill);
break;
if (cashQuantity != decimal.Zero)
{
security.SettlementModel.ApplyFunds(new ApplyFundsSettlementModelParameters(portfolio, security, fill.UtcTime, new CashAmount(cashQuantity, option.QuoteCurrency.Symbol), fill));
return new ConvertibleCashAmount(cashQuantity + baseResult.Amount, option.QuoteCurrency);
}
}
}
return baseResult;
}
}
}
45 changes: 28 additions & 17 deletions Common/Securities/SecurityPortfolioModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ public virtual void ProcessFill(SecurityPortfolioManager portfolio, Security sec
var closedPosition = false;
//Make local decimals to avoid any rounding errors from int multiplication
var quantityHoldings = (decimal)security.Holdings.Quantity;
var absoluteHoldingsQuantity = security.Holdings.AbsoluteQuantity;
var averageHoldingsPrice = security.Holdings.AveragePrice;

try
Expand Down Expand Up @@ -87,24 +86,10 @@ public virtual void ProcessFill(SecurityPortfolioManager portfolio, Security sec
// calculate the last trade profit
if (closedPosition)
{
// profit = (closed sale value - cost)*conversion to account currency
// closed sale value = quantity closed * fill price BUYs are deemed negative cash flow
// cost = quantity closed * average holdings price SELLS are deemed positive cash flow
var absoluteQuantityClosed = Math.Min(fill.AbsoluteFillQuantity, absoluteHoldingsQuantity);
var quantityClosed = Math.Sign(-fill.FillQuantity) * absoluteQuantityClosed;
var closedCost = security.Holdings.GetQuantityValue(quantityClosed, averageHoldingsPrice);
var closedSaleValueInQuoteCurrency = security.Holdings.GetQuantityValue(quantityClosed, fill.FillPrice);

var lastTradeProfit = closedSaleValueInQuoteCurrency.Amount - closedCost.Amount;
var lastTradeProfitInAccountCurrency = closedSaleValueInQuoteCurrency.InAccountCurrency - closedCost.InAccountCurrency;

// Reflect account cash adjustment for futures/CFD position
if (security.Type == SecurityType.Future || security.Type == SecurityType.Cfd || security.Type == SecurityType.CryptoFuture)
{
security.SettlementModel.ApplyFunds(new ApplyFundsSettlementModelParameters(portfolio, security, fill.UtcTime, new CashAmount(lastTradeProfit, closedCost.Cash.Symbol), fill));
}
var lastTradeProfit = ProcessCloseTradeProfit(portfolio, security, fill);

//Update Vehicle Profit Tracking:
var lastTradeProfitInAccountCurrency = lastTradeProfit.InAccountCurrency;
security.Holdings.AddNewProfit(lastTradeProfitInAccountCurrency);
security.Holdings.SetLastTradeProfit(lastTradeProfitInAccountCurrency);
var transactionProfitLoss = lastTradeProfitInAccountCurrency - 2 * feeInAccountCurrency;
Expand Down Expand Up @@ -185,5 +170,31 @@ public virtual void ProcessFill(SecurityPortfolioManager portfolio, Security sec
//Set the results back to the vehicle.
security.Holdings.SetHoldings(averageHoldingsPrice, quantityHoldings);
}

/// <summary>
/// Helper method to determine the close trade profit
/// </summary>
protected virtual ConvertibleCashAmount ProcessCloseTradeProfit(SecurityPortfolioManager portfolio, Security security, OrderEvent fill)
{
var absoluteHoldingsQuantity = security.Holdings.AbsoluteQuantity;

// profit = (closed sale value - cost)*conversion to account currency
// closed sale value = quantity closed * fill price BUYs are deemed negative cash flow
// cost = quantity closed * average holdings price SELLS are deemed positive cash flow
var absoluteQuantityClosed = Math.Min(fill.AbsoluteFillQuantity, absoluteHoldingsQuantity);
var quantityClosed = Math.Sign(-fill.FillQuantity) * absoluteQuantityClosed;
var closedCost = security.Holdings.GetQuantityValue(quantityClosed, security.Holdings.AveragePrice);
var closedSaleValueInQuoteCurrency = security.Holdings.GetQuantityValue(quantityClosed, fill.FillPrice);

var lastTradeProfit = new ConvertibleCashAmount(closedSaleValueInQuoteCurrency.Amount - closedCost.Amount, closedSaleValueInQuoteCurrency.Cash);

// Reflect account cash adjustment for futures/CFD position
if (security.Type == SecurityType.Future || security.Type == SecurityType.Cfd || security.Type == SecurityType.CryptoFuture)
{
security.SettlementModel.ApplyFunds(new ApplyFundsSettlementModelParameters(portfolio, security, fill.UtcTime, lastTradeProfit, fill));
}

return lastTradeProfit;
}
}
}
Loading

0 comments on commit 6982f67

Please sign in to comment.