Lean  $LEAN_TAG$
DefaultOptionAssignmentModel.cs
1 /*
2  * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
3  * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software
10  * distributed under the License is distributed on an "AS IS" BASIS,
11  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12  * See the License for the specific language governing permissions and
13  * limitations under the License.
14 */
15 
16 using System;
17 using QuantConnect.Orders;
19 
21 {
22  /// <summary>
23  /// The option assignment model emulates exercising of short option positions in the portfolio.
24  /// Simulator implements basic no-arb argument: when time value of the option contract is close to zero
25  /// it assigns short legs getting profit close to expiration dates in deep ITM positions. User algorithm then receives
26  /// assignment event from LEAN. Simulator randomly scans for arbitrage opportunities every two hours or so.
27  /// </summary>
29  {
30  // when we start simulating assignments prior to expiration
31  private readonly TimeSpan _priorExpiration;
32 
33  // we focus only on deep ITM calls and puts
34  private readonly decimal _requiredInTheMoneyPercent;
35 
36  /// <summary>
37  /// Creates a new instance
38  /// </summary>
39  /// <param name="requiredInTheMoneyPercent">The percent in the money the option has to be to trigger the option assignment</param>
40  /// <param name="priorExpiration">For <see cref="OptionStyle.American"/>, the time span prior to expiration were we will try to evaluate option assignment</param>
41  public DefaultOptionAssignmentModel(decimal requiredInTheMoneyPercent = 0.05m, TimeSpan? priorExpiration = null)
42  {
43  _priorExpiration = priorExpiration ?? new TimeSpan(4, 0, 0, 0);
44  _requiredInTheMoneyPercent = requiredInTheMoneyPercent;
45  }
46 
47  /// <summary>
48  /// Get's the option assignments to generate if any
49  /// </summary>
50  /// <param name="parameters">The option assignment parameters data transfer class</param>
51  /// <returns>The option assignment result</returns>
53  {
54  var option = parameters.Option;
55  var underlying = parameters.Option.Underlying;
56 
57  // we take only options that expire soon
58  if ((option.Symbol.ID.OptionStyle == OptionStyle.American && option.Symbol.ID.Date - option.LocalTime <= _priorExpiration ||
59  option.Symbol.ID.OptionStyle == OptionStyle.European && option.Symbol.ID.Date.Date == option.LocalTime.Date)
60  // we take only deep ITM strikes
61  && IsDeepInTheMoney(option))
62  {
63  // we estimate P/L
64  var potentialPnL = EstimateArbitragePnL(option, (OptionHolding)option.Holdings, underlying);
65  if (potentialPnL > 0)
66  {
67  return new OptionAssignmentResult(option.Holdings.AbsoluteQuantity, "Simulated option assignment before expiration");
68  }
69  }
70 
72  }
73 
74  private bool IsDeepInTheMoney(Option option)
75  {
76  var symbol = option.Symbol;
77  var underlyingPrice = option.Underlying.Close;
78 
79  // For some options, the price is based on a fraction of the underlying, such as for NQX.
80  // Therefore, for those options we need to scale the price when comparing it with the
81  // underlying. For that reason we use option.ScaledStrikePrice instead of
82  // option.StrikePrice
83  var result =
84  symbol.ID.OptionRight == OptionRight.Call
85  ? (underlyingPrice - option.ScaledStrikePrice) / underlyingPrice > _requiredInTheMoneyPercent
86  : (option.ScaledStrikePrice - underlyingPrice) / underlyingPrice > _requiredInTheMoneyPercent;
87 
88  return result;
89  }
90 
91  private static decimal EstimateArbitragePnL(Option option, OptionHolding holding, Security underlying)
92  {
93  // no-arb argument:
94  // if our long deep ITM position has a large B/A spread and almost no time value, it may be interesting for us
95  // to exercise the option and close the resulting position in underlying instrument, if we want to exit now.
96 
97  // User's short option position is our long one.
98  // In order to sell ITM position we take option bid price as an input
99  var optionPrice = option.BidPrice;
100 
101  // we are interested in underlying bid price if we exercise calls and want to sell the underlying immediately.
102  // we are interested in underlying ask price if we exercise puts
103  var underlyingPrice = option.Symbol.ID.OptionRight == OptionRight.Call
104  ? underlying.BidPrice
105  : underlying.AskPrice;
106 
107  // quantity is normally negative algo's holdings, but since we're modeling the contract holder (counter-party)
108  // it's negative THEIR holdings. holding.Quantity is negative, so if counter-party exercises, they would reduce holdings
109  var underlyingQuantity = option.GetExerciseQuantity(holding.Quantity);
110 
111  // Scenario 1 (base): we just close option position
112  var marketOrder1 = new MarketOrder(option.Symbol, -holding.Quantity, option.LocalTime.ConvertToUtc(option.Exchange.TimeZone));
113  var orderFee1 = option.FeeModel.GetOrderFee(
114  new OrderFeeParameters(option, marketOrder1)).Value.Amount * option.QuoteCurrency.ConversionRate;
115 
116  var basePnL = (optionPrice - holding.AveragePrice) * -holding.Quantity
117  * option.QuoteCurrency.ConversionRate
118  * option.SymbolProperties.ContractMultiplier
119  - orderFee1;
120 
121  // Scenario 2 (alternative): we exercise option and then close underlying position
122  var optionExerciseOrder2 = new OptionExerciseOrder(option.Symbol, (int)holding.AbsoluteQuantity, option.LocalTime.ConvertToUtc(option.Exchange.TimeZone));
123  var optionOrderFee2 = option.FeeModel.GetOrderFee(
124  new OrderFeeParameters(option, optionExerciseOrder2)).Value.Amount * option.QuoteCurrency.ConversionRate;
125 
126  var underlyingOrderFee2Amount = 0m;
127 
128  // Cash settlements do not open a position for the underlying.
129  // For Physical Delivery, we calculate the order fee since we have to close the position
130  if (option.ExerciseSettlement == SettlementType.PhysicalDelivery)
131  {
132  var underlyingMarketOrder2 = new MarketOrder(underlying.Symbol, -underlyingQuantity,
133  underlying.LocalTime.ConvertToUtc(underlying.Exchange.TimeZone));
134  var underlyingOrderFee2 = underlying.FeeModel.GetOrderFee(
135  new OrderFeeParameters(underlying, underlyingMarketOrder2)).Value.Amount * underlying.QuoteCurrency.ConversionRate;
136  underlyingOrderFee2Amount = underlyingOrderFee2;
137  }
138 
139  // calculating P/L of the two transactions (exercise option and then close underlying position)
140  var altPnL = (underlyingPrice - option.ScaledStrikePrice) * underlyingQuantity * underlying.QuoteCurrency.ConversionRate * option.ContractUnitOfTrade
141  - underlyingOrderFee2Amount
142  - holding.AveragePrice * holding.AbsoluteQuantity * option.SymbolProperties.ContractMultiplier * option.QuoteCurrency.ConversionRate
143  - optionOrderFee2;
144 
145  return altPnL - basePnL;
146  }
147  }
148 }