From 9aeae1f382afa52ac47e85d4811a1f5b9e043f98 Mon Sep 17 00:00:00 2001 From: Kapil Yadav Date: Sun, 19 Jul 2026 15:26:07 +0530 Subject: [PATCH 1/2] Fix option assignment trade statistics Resolve the security from each order event when updating TradeBuilder so physically settled underlying fills use the underlying multiplier and conversion rate. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 716a0df4-0117-458b-b4ac-7d8aeeb9bf48 --- .../OptionAssignmentRegressionAlgorithm.cs | 19 +++++++++++++++++++ .../BrokerageTransactionHandler.cs | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/Algorithm.CSharp/OptionAssignmentRegressionAlgorithm.cs b/Algorithm.CSharp/OptionAssignmentRegressionAlgorithm.cs index bd46d9aec1d5..8196ac03840a 100644 --- a/Algorithm.CSharp/OptionAssignmentRegressionAlgorithm.cs +++ b/Algorithm.CSharp/OptionAssignmentRegressionAlgorithm.cs @@ -14,11 +14,13 @@ * */ +using System; using System.Collections.Generic; using System.Linq; using QuantConnect.Data; using QuantConnect.Interfaces; using QuantConnect.Securities; +using QuantConnect.Statistics; namespace QuantConnect.Algorithm.CSharp { @@ -79,6 +81,23 @@ public override void OnData(Slice slice) } } + public override void OnEndOfAlgorithm() + { + foreach (var trade in TradeBuilder.ClosedTrades.Where(trade => trade.Symbols.Contains(Stock.Symbol))) + { + var direction = trade.Direction == TradeDirection.Long ? 1m : -1m; + var expectedProfitLoss = Math.Round( + (trade.ExitPrice - trade.EntryPrice) * trade.Quantity * direction * Stock.SymbolProperties.ContractMultiplier, + 2); + + if (trade.ProfitLoss != expectedProfitLoss) + { + throw new RegressionTestException( + $"Expected underlying trade profit/loss to be {expectedProfitLoss}. Actual: {trade.ProfitLoss}"); + } + } + } + /// /// This is used by the regression test system to indicate if the open source Lean repository has the required data to run this algorithm. /// diff --git a/Engine/TransactionHandlers/BrokerageTransactionHandler.cs b/Engine/TransactionHandlers/BrokerageTransactionHandler.cs index 08e84ecf1178..d85e322b6774 100644 --- a/Engine/TransactionHandlers/BrokerageTransactionHandler.cs +++ b/Engine/TransactionHandlers/BrokerageTransactionHandler.cs @@ -1313,7 +1313,7 @@ private void HandleOrderEvents(List orderEvents) if (orderEvent.Status == OrderStatus.Filled || orderEvent.Status == OrderStatus.PartiallyFilled) { - var security = orders[i].Security; + var security = _algorithm.Securities[orderEvent.Symbol]; var multiplier = security.SymbolProperties.ContractMultiplier; var securityConversionRate = security.QuoteCurrency.ConversionRate; From e6624eadc8fca11b41ecf3b0d76039c38d4f40a1 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Mon, 20 Jul 2026 11:41:37 -0400 Subject: [PATCH 2/2] Resolve order event security from the event symbol Option exercises emit the underlying fill under the option's order id, so resolving the security from the order symbol handed the option's contract multiplier and quote currency conversion rate to the underlying fill, inflating closed trade statistics. Extend the option assignment regression algorithm, in both C# and Python, to assert every closed trade's profit and loss against its own security's contract multiplier. --- .../OptionAssignmentRegressionAlgorithm.cs | 26 ++++++++++++++----- .../OptionAssignmentRegressionAlgorithm.py | 19 ++++++++++++++ .../BrokerageTransactionHandler.cs | 7 +++-- 3 files changed, 44 insertions(+), 8 deletions(-) diff --git a/Algorithm.CSharp/OptionAssignmentRegressionAlgorithm.cs b/Algorithm.CSharp/OptionAssignmentRegressionAlgorithm.cs index 8196ac03840a..854714fe1b20 100644 --- a/Algorithm.CSharp/OptionAssignmentRegressionAlgorithm.cs +++ b/Algorithm.CSharp/OptionAssignmentRegressionAlgorithm.cs @@ -81,19 +81,33 @@ public override void OnData(Slice slice) } } + private Security GetSecurity(Symbol symbol) + { + if (symbol == Stock.Symbol) + { + return Stock; + } + if (symbol == CallOptionSymbol) + { + return CallOption; + } + if (symbol == PutOptionSymbol) + { + return PutOption; + } + throw new RegressionTestException($"Unexpected symbol: {symbol}"); + } + public override void OnEndOfAlgorithm() { - foreach (var trade in TradeBuilder.ClosedTrades.Where(trade => trade.Symbols.Contains(Stock.Symbol))) + foreach (var trade in TradeBuilder.ClosedTrades) { var direction = trade.Direction == TradeDirection.Long ? 1m : -1m; - var expectedProfitLoss = Math.Round( - (trade.ExitPrice - trade.EntryPrice) * trade.Quantity * direction * Stock.SymbolProperties.ContractMultiplier, - 2); + var expectedProfitLoss = Math.Round((trade.ExitPrice - trade.EntryPrice) * trade.Quantity * direction * GetSecurity(trade.Symbols.Single()).SymbolProperties.ContractMultiplier, 2); if (trade.ProfitLoss != expectedProfitLoss) { - throw new RegressionTestException( - $"Expected underlying trade profit/loss to be {expectedProfitLoss}. Actual: {trade.ProfitLoss}"); + throw new RegressionTestException($"Expected underlying trade profit/loss to be {expectedProfitLoss}. Actual: {trade.ProfitLoss}"); } } } diff --git a/Algorithm.Python/OptionAssignmentRegressionAlgorithm.py b/Algorithm.Python/OptionAssignmentRegressionAlgorithm.py index cb492e8a7a33..af206edf4fe5 100644 --- a/Algorithm.Python/OptionAssignmentRegressionAlgorithm.py +++ b/Algorithm.Python/OptionAssignmentRegressionAlgorithm.py @@ -47,3 +47,22 @@ def on_data(self, data): if self.time < self.call_option_symbol.id.date: self.market_order(self.call_option_symbol, -1) + + def get_security(self, symbol): + if symbol == self.stock.symbol: + return self.stock + if symbol == self.call_option_symbol: + return self.call_option + if symbol == self.put_option_symbol: + return self.put_option + raise RegressionTestException(f"Unexpected symbol: {symbol}") + + def on_end_of_algorithm(self): + for trade in self.trade_builder.closed_trades: + symbol, = trade.symbols + direction = 1 if trade.direction == TradeDirection.LONG else -1 + multiplier = self.get_security(symbol).symbol_properties.contract_multiplier + expected_profit_loss = round((trade.exit_price - trade.entry_price) * trade.quantity * direction * multiplier, 2) + + if trade.profit_loss != expected_profit_loss: + raise RegressionTestException(f"Expected underlying trade profit/loss to be {expected_profit_loss}. Actual: {trade.profit_loss}") diff --git a/Engine/TransactionHandlers/BrokerageTransactionHandler.cs b/Engine/TransactionHandlers/BrokerageTransactionHandler.cs index d85e322b6774..8477400c1158 100644 --- a/Engine/TransactionHandlers/BrokerageTransactionHandler.cs +++ b/Engine/TransactionHandlers/BrokerageTransactionHandler.cs @@ -1186,7 +1186,10 @@ private void HandleOrderEvents(List orderEvents) return; } - orders.Add(new OpenOrderState(order, ticket, security ?? _algorithm.Securities[order.Symbol])); + // we take the order event symbol security to make sure to match the actual traded asset: + // for order exercises, the underlying security events are traded under the same option order id, + // but the order event symbol is the underlying security, not the option contract. + orders.Add(new OpenOrderState(order, ticket, security != null && security.Symbol == orderEvent.Symbol ? security : _algorithm.Securities[orderEvent.Symbol])); orderEvent.Ticket = ticket; } @@ -1313,7 +1316,7 @@ private void HandleOrderEvents(List orderEvents) if (orderEvent.Status == OrderStatus.Filled || orderEvent.Status == OrderStatus.PartiallyFilled) { - var security = _algorithm.Securities[orderEvent.Symbol]; + var security = orders[i].Security; var multiplier = security.SymbolProperties.ContractMultiplier; var securityConversionRate = security.QuoteCurrency.ConversionRate;