Hi,
Newbie here, trying out a random strategy just to get the hang of the platform. Big thanks to the QC team for building such a cool product and for the extensive documentation and tutorials.
My strategy simply writes an OTM call on SPY on each expiration day, if SPY is near it's 52W high. If it gets assigned it covers the short ASAP the following day. I notice however that the portfolio ends up still carrying around some older options that should have expired. These use up the simulated buying power and cause placing further order to fail:
2020-11-18 09:31:00 : SPY 181128C00272500 -5.0 @ 0.03
2020-11-18 09:31:00 : SPY 180725C00284000 -5.0 @ 0.01
2020-11-18 09:31:00 : SPY 181107C00280000 -5.0 @ 0.13
2020-11-18 09:31:00 : SPY 181017C00279000 -5.0 @ 1.63
2020-11-18 09:31:00 : Time: 11/18/2020 14:31:00 OrderID: 1261 EventID: 1 Symbol: SPY 201118C00363000 Status: Invalid Quantity: -5 Message: Order Error: id: 1261, Insufficient buying power to complete order (Value:-117.5), Reason: Id: 1261, Initial Margin: -118.75, Free Margin: 0 IsAssignment: False
The full backtest is also attached. Please let me know if I can provide anything else to help debug the problem.
Thanks,
Karthik
Karthik Kailash
Pretty sure I found the issue. The option expiring 2018-07-25 is being assigned, but the system thinks there is not enough buying power to do so, since the option itself is taking up buying power. I believe this is an error, since the buying power of the assigned security should replace the buying power of the option.
The logs below show the order for selling the option, followed by the failed assignment attempt, followed by a log showing the option still inside the portfolio. It only happens at this point probably because this is when SPY's value reaches a point where the account's buying power can't hold both the option and the assigned security.
2018-07-25 09:31:00 Time: 07/25/2018 13:31:00 OrderID: 908 EventID: 1 Symbol: SPY 180725C00284000 Status: Submitted Quantity: -5 IsAssignment: False 2018-07-25 09:31:00 Time: 07/25/2018 13:31:00 OrderID: 908 EventID: 2 Symbol: SPY 180725C00284000 Status: Filled Quantity: -5 FillQuantity: -5 FillPrice: 0.01 USD OrderFee: 1.25 USD IsAssignment: False 2018-07-25 16:00:00 Time: 07/25/2018 20:00:00 OrderID: 909 EventID: 1 Symbol: SPY 180725C00284000 Status: Invalid Quantity: 5 Message: Order Error: id: 909, Insufficient buying power to complete order (Value:-142000), Reason: Id: 909, Initial Margin: -71005, Free Margin: 70814 IsAssignment: False 2018-07-25 16:00:00 Order Error: id: 909, Insufficient buying power to complete order (Value:-142000), Reason: Id: 909, Initial Margin: -71005, Free Margin: 70814 2018-07-27 09:31:00 SPY 180725C00284000 -5.0 @ 0.01
I tried another backtest, doubling the leverage on the SPY security, and the problem went away.
@QC team - this seems like a bug, please let me know if and where I should file a bug report.
Karthik Kailash
I added an optimization to only subscribe to the single option that the algorithm wants to trade. This makes the backtest run a lot faster, but now I get the same issue of leftover expired options in the porfolio.
This time, the logs look different. It looks like the order for expiring the option gets cancelled right after it is submitted. Not sure why it doesn't happen every time. You can see in the logs snippet below the first few lines show a successful case of selling an option and it then expiring. Then the final lines show the expiry order being cancelled:
2010-11-12 09:31:00 : Time: 11/12/2010 14:31:00 OrderID: 37 EventID: 1 Symbol: SPY 101112C00127000 Status: Submitted Quantity: -5 IsAssignment: False 2010-11-12 09:31:00 : Time: 11/12/2010 14:31:00 OrderID: 37 EventID: 2 Symbol: SPY 101112C00127000 Status: Filled Quantity: -5 FillQuantity: -5 FillPrice: 0.01 USD OrderFee: 1.25 USD IsAssignment: False 2010-11-12 16:00:00 : Time: 11/12/2010 21:00:00 OrderID: 38 EventID: 1 Symbol: SPY 101112C00127000 Status: Submitted Quantity: 5 IsAssignment: False 2010-11-13 00:00:00 : Time: 11/13/2010 05:00:00 OrderID: 38 EventID: 2 Symbol: SPY 101112C00127000 Status: Filled Quantity: 5 FillQuantity: 5 FillPrice: 0 Message: OTM IsAssignment: False 2010-11-26 09:31:00 : 2010-11-26 00:00:00 126.0 0 2010-11-26 09:31:00 : Time: 11/26/2010 14:31:00 OrderID: 39 EventID: 1 Symbol: SPY 101126C00126000 Status: Submitted Quantity: -5 IsAssignment: False 2010-11-26 09:31:00 : Time: 11/26/2010 14:31:00 OrderID: 39 EventID: 2 Symbol: SPY 101126C00126000 Status: Filled Quantity: -5 FillQuantity: -5 FillPrice: 0.01 USD OrderFee: 1.25 USD IsAssignment: False 2010-11-27 00:00:00 : Time: 11/27/2010 05:00:00 OrderID: 40 EventID: 1 Symbol: SPY 101126C00126000 Status: Submitted Quantity: 5 IsAssignment: False 2010-11-27 00:00:00 : Time: 11/27/2010 05:00:00 OrderID: 40 EventID: 2 Symbol: SPY 101126C00126000 Status: CancelPending Quantity: 5 IsAssignment: False 2010-11-27 00:00:00 : Time: 11/27/2010 05:00:00 OrderID: 40 EventID: 3 Symbol: SPY 101126C00126000 Status: Canceled Quantity: 5 IsAssignment: False
Karthik Kailash
Found a workaround by overriding the option's margin model with one that overrides GetReservedBuyingPowerForPosition to return 0 if the option has been delisted, and delegates to the original margin model for everything else:
class MyOptionMarginModel: option = None originalMarginModel = None def __init__(self, option, originalMarginModel): self.option = option self.originalMarginModel = originalMarginModel def GetLeverage(self, security): return self.originalMarginModel.GetLeverage(security) def SetLeverage(self, security, leverage): self.originalMarginModel.SetLeverage(security, leverage) def HasSufficientBuyingPowerForOrder(self, parameters): return self.originalMarginModel.HasSufficientBuyingPowerForOrder(parameters) def GetMaximumOrderQuantityForTargetBuyingPower(self, parameters): return self.originalMarginModel.GetMaximumOrderQuantityForTargetBuyingPower(parameters) def GetMaximumOrderQuantityForDeltaBuyingPower(self, parameters): return self.originalMarginModel.GetMaximumOrderQuantityForDeltaBuyingPower(parameters) def GetReservedBuyingPowerForPosition(self, params): if self.option.IsDelisted: return ReservedBuyingPowerForPosition(0) else: return self.originalMarginModel.GetReservedBuyingPowerForPosition(params) def GetBuyingPower(self, parameters): return self.originalMarginModel.GetBuyingPower(parameters)
Now, the portfolio can accumulate the delisted options without them affecting the buying power. Hopefully someone can figure out what the underlying problem is and fix it so that we don't need such a hack
Cole S
They added a CustomBuyingPowerModel last month so you can ignore the buying power. They treat each leg of a strategy separately rather than a unit currently, so you have to ignore the margin model to make it work. Link below
https://github.com/QuantConnect/Lean/blob/84264ca7ef1f9f6f416c5ac58a4aec110b48b67c/Algorithm.Python/CustomBuyingPowerModelAlgorithm.pyKarthik Kailash
Thanks kctrader . That's a simpler model to follow. Here is my custom option margin model with workarounds for both the delisting issue and assignment issue:
from QuantConnect.Orders import * from QuantConnect.Securities import * class OptionMarginModelWithDelistingAndExerciseFix(BuyingPowerModel): algorithm = None option = None originalMarginModel = None exercised = False def __init__(self, algorithm, option, originalMarginModel): self.algorithm = algorithm self.option = option self.originalMarginModel = originalMarginModel def HasSufficientBuyingPowerForOrder(self, parameters): # Workaround for a bug in LEAN: For an option exercise or assignment, it ends up checking to see if there is enough room to bring in # the new underlying's position. However, it doesn't remove the option from the portfolio when doing this check so it ends # up double counting the option's used buying power and the underlying's buying power. So, we return 0 for the buying power # when exercise happens. Note that this assumes that the entire position is being exercised. if parameters.Order.Type == OrderType.OptionExercise: self.exercised = True return self.originalMarginModel.HasSufficientBuyingPowerForOrder(parameters) def GetReservedBuyingPowerForPosition(self, params): if self.option.IsDelisted or self.exercised: return ReservedBuyingPowerForPosition(0) else: return self.originalMarginModel.GetReservedBuyingPowerForPosition(params)
Karthik Kailash
The material on this website is provided for informational purposes only and does not constitute an offer to sell, a solicitation to buy, or a recommendation or endorsement for any security or strategy, nor does it constitute an offer to provide investment advisory services by QuantConnect. In addition, the material offers no opinion with respect to the suitability of any security or specific investment. QuantConnect makes no guarantees as to the accuracy or completeness of the views expressed in the website. The views are subject to change, and may have become unreliable for various reasons, including changes in market conditions or economic circumstances. All investments involve risk, including loss of principal. You should consult with an investment professional before making any investment decisions.
To unlock posting to the community forums please complete at least 30% of Boot Camp.
You can continue your Boot Camp training progress from the terminal. We hope to see you in the community soon!