Skip to content

Commit 4249165

Browse files
YadavKapilKapil Yadavjhonabreul
authored
Fix trade statistics for option assignment underlying fills (#9627)
* 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 * 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. --------- Co-authored-by: Kapil Yadav <kapyadav@microsoft.com> Co-authored-by: Jhonathan Abreu <jdabreu25@gmail.com>
1 parent 0269115 commit 4249165

3 files changed

Lines changed: 56 additions & 1 deletion

File tree

Algorithm.CSharp/OptionAssignmentRegressionAlgorithm.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,13 @@
1414
*
1515
*/
1616

17+
using System;
1718
using System.Collections.Generic;
1819
using System.Linq;
1920
using QuantConnect.Data;
2021
using QuantConnect.Interfaces;
2122
using QuantConnect.Securities;
23+
using QuantConnect.Statistics;
2224

2325
namespace QuantConnect.Algorithm.CSharp
2426
{
@@ -79,6 +81,37 @@ public override void OnData(Slice slice)
7981
}
8082
}
8183

84+
private Security GetSecurity(Symbol symbol)
85+
{
86+
if (symbol == Stock.Symbol)
87+
{
88+
return Stock;
89+
}
90+
if (symbol == CallOptionSymbol)
91+
{
92+
return CallOption;
93+
}
94+
if (symbol == PutOptionSymbol)
95+
{
96+
return PutOption;
97+
}
98+
throw new RegressionTestException($"Unexpected symbol: {symbol}");
99+
}
100+
101+
public override void OnEndOfAlgorithm()
102+
{
103+
foreach (var trade in TradeBuilder.ClosedTrades)
104+
{
105+
var direction = trade.Direction == TradeDirection.Long ? 1m : -1m;
106+
var expectedProfitLoss = Math.Round((trade.ExitPrice - trade.EntryPrice) * trade.Quantity * direction * GetSecurity(trade.Symbols.Single()).SymbolProperties.ContractMultiplier, 2);
107+
108+
if (trade.ProfitLoss != expectedProfitLoss)
109+
{
110+
throw new RegressionTestException($"Expected underlying trade profit/loss to be {expectedProfitLoss}. Actual: {trade.ProfitLoss}");
111+
}
112+
}
113+
}
114+
82115
/// <summary>
83116
/// This is used by the regression test system to indicate if the open source Lean repository has the required data to run this algorithm.
84117
/// </summary>

Algorithm.Python/OptionAssignmentRegressionAlgorithm.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,22 @@ def on_data(self, data):
4747

4848
if self.time < self.call_option_symbol.id.date:
4949
self.market_order(self.call_option_symbol, -1)
50+
51+
def get_security(self, symbol):
52+
if symbol == self.stock.symbol:
53+
return self.stock
54+
if symbol == self.call_option_symbol:
55+
return self.call_option
56+
if symbol == self.put_option_symbol:
57+
return self.put_option
58+
raise RegressionTestException(f"Unexpected symbol: {symbol}")
59+
60+
def on_end_of_algorithm(self):
61+
for trade in self.trade_builder.closed_trades:
62+
symbol, = trade.symbols
63+
direction = 1 if trade.direction == TradeDirection.LONG else -1
64+
multiplier = self.get_security(symbol).symbol_properties.contract_multiplier
65+
expected_profit_loss = round((trade.exit_price - trade.entry_price) * trade.quantity * direction * multiplier, 2)
66+
67+
if trade.profit_loss != expected_profit_loss:
68+
raise RegressionTestException(f"Expected underlying trade profit/loss to be {expected_profit_loss}. Actual: {trade.profit_loss}")

Engine/TransactionHandlers/BrokerageTransactionHandler.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1186,7 +1186,10 @@ private void HandleOrderEvents(List<OrderEvent> orderEvents)
11861186
return;
11871187
}
11881188

1189-
orders.Add(new OpenOrderState(order, ticket, security ?? _algorithm.Securities[order.Symbol]));
1189+
// we take the order event symbol security to make sure to match the actual traded asset:
1190+
// for order exercises, the underlying security events are traded under the same option order id,
1191+
// but the order event symbol is the underlying security, not the option contract.
1192+
orders.Add(new OpenOrderState(order, ticket, security != null && security.Symbol == orderEvent.Symbol ? security : _algorithm.Securities[orderEvent.Symbol]));
11901193
orderEvent.Ticket = ticket;
11911194
}
11921195

0 commit comments

Comments
 (0)