diff --git a/Brokerages/Brokerage.cs b/Brokerages/Brokerage.cs index 0d19a4d8be50..29e9c33f066a 100644 --- a/Brokerages/Brokerage.cs +++ b/Brokerages/Brokerage.cs @@ -23,6 +23,7 @@ using System.Threading.Tasks; using QuantConnect.Interfaces; using QuantConnect.Securities; +using QuantConnect.Securities.Option; using QuantConnect.Orders.Fees; using System.Collections.Generic; using System.Collections.Concurrent; @@ -509,13 +510,6 @@ protected virtual List GetCashBalance(Dictionary bro /// public virtual bool AccountInstantlyUpdated => false; - /// - /// True when Lean syncs the option holdings with the brokerage once a day, for the brokerages that report - /// nothing when a contract expires or is assigned. A brokerage that reports every option position change - /// itself returns false - /// - public virtual bool HoldingsSyncEnabled => true; - /// /// Cache holding the legs of a combo order until all of them have been placed, so the brokerage can submit them together /// @@ -589,7 +583,7 @@ public virtual bool ShouldPerformCashSync(DateTime currentTimeUtc) } /// - /// Synchronizes the cashbook with the brokerage account + /// Synchronizes the cashbook, and the option holdings, with the brokerage account /// /// The algorithm instance /// The current time (UTC) @@ -606,6 +600,8 @@ public virtual bool PerformCashSync(IAlgorithm algorithm, DateTime currentTimeUt return false; } + PerformOptionHoldingsSync(algorithm, currentTimeUtc); + Log.Trace("Brokerage.PerformCashSync(): Sync cash balance"); List balances = null; @@ -698,6 +694,79 @@ public virtual bool PerformCashSync(IAlgorithm algorithm, DateTime currentTimeUt return true; } + /// + /// Synchronizes the option holdings with the brokerage account, once a day with the cash sync, for the brokerages + /// that report nothing when a contract expires or is assigned: a contract the brokerage no longer lists is raised as + /// an option notification with position 0. A brokerage that reports every option position change itself overrides + /// this method and returns true + /// + /// The algorithm instance + /// The current time (UTC) + /// True if the option holdings sync was performed successfully + public virtual bool PerformOptionHoldingsSync(IAlgorithm algorithm, DateTime currentTimeUtc) + { + try + { + var brokerageHoldings = default(Dictionary); + foreach (var security in algorithm.Securities.Values) + { + if (!security.Invested || !security.Symbol.SecurityType.IsOption()) + { + continue; + } + + if (brokerageHoldings == null) + { + Log.Trace("Brokerage.PerformOptionHoldingsSync(): Start syncing option holdings..."); + brokerageHoldings = []; + foreach (var holding in GetAccountHoldings()) + { + if (holding.Symbol.SecurityType.IsOption()) + { + brokerageHoldings.Add(holding.Symbol, holding.Quantity); + } + } + } + + var position = 0m; + if (!OptionSymbol.IsOptionContractExpired(security.Symbol, currentTimeUtc)) + { + brokerageHoldings.TryGetValue(security.Symbol, out position); + } + + if (position != security.Holdings.Quantity) + { + var underlying = (security as Option).Underlying; + if (underlying.Price == 0) + { + var lastKnownPrice = algorithm.GetLastKnownPrice(underlying.Symbol); + underlying.SetMarketPrice(lastKnownPrice); + if (underlying.Price == 0) + { + Log.Error($"Brokerage.PerformOptionHoldingsSync(): no price for {underlying.Symbol.Value}, {security.Symbol.Value} not synced."); + continue; + } + } + + Log.Trace($"Brokerage.PerformOptionHoldingsSync(): syncing option holding: Symbol: {security.Symbol.Value}, Holdings: {security.Holdings.Quantity}, Position: {position}"); + OnOptionNotification(new OptionNotificationEventArgs(security.Symbol, position)); + } + } + + if (brokerageHoldings != null) + { + Log.Trace($"Brokerage.PerformOptionHoldingsSync(): Brokerage option holdings count {brokerageHoldings.Count}. Exit"); + } + + return true; + } + catch (Exception err) + { + Log.Error(err, "Brokerage.PerformOptionHoldingsSync(): failed to sync the option holdings."); + return false; + } + } + #endregion #region CrossZeroOrder implementation diff --git a/Engine/TransactionHandlers/BrokerageTransactionHandler.cs b/Engine/TransactionHandlers/BrokerageTransactionHandler.cs index 48e006b2973f..4e923bd59a28 100644 --- a/Engine/TransactionHandlers/BrokerageTransactionHandler.cs +++ b/Engine/TransactionHandlers/BrokerageTransactionHandler.cs @@ -748,7 +748,6 @@ public virtual void ProcessSynchronousEvents() // only perform cash syncs if we haven't had a fill for at least 10 seconds if (TimeSinceLastFill > TimeSpan.FromSeconds(10)) { - SyncOptionHoldings(); if (!_brokerage.PerformCashSync(_algorithm, CurrentTimeUtc, () => TimeSinceLastFill)) { if (++_failedCashSyncAttempts >= MaxCashSyncAttempts) @@ -1734,76 +1733,6 @@ private void HandleDelistingNotification(DelistingNotificationEventArgs e) } } - /// - /// Syncs the option holdings with the brokerage once a day, with the cash sync, for the brokerages that report - /// nothing when a contract expires or is assigned - /// - private void SyncOptionHoldings() - { - if (_brokerage is Brokerage { HoldingsSyncEnabled: false }) - { - return; - } - - try - { - var brokerageHoldings = default(Dictionary); - foreach (var security in _algorithm.Securities.Values) - { - if (!security.Invested || !security.Symbol.SecurityType.IsOption()) - { - continue; - } - - if (brokerageHoldings == null) - { - Log.Trace("BrokerageTransactionHandler.SyncOptionHoldings(): Start syncing option holdings..."); - brokerageHoldings = []; - foreach (var holding in _brokerage.GetAccountHoldings()) - { - if (holding.Symbol.SecurityType.IsOption()) - { - brokerageHoldings.Add(holding.Symbol, holding.Quantity); - } - } - } - - var position = 0m; - if (!OptionSymbol.IsOptionContractExpired(security.Symbol, CurrentTimeUtc)) - { - brokerageHoldings.TryGetValue(security.Symbol, out position); - } - - if (position != security.Holdings.Quantity) - { - var underlying = (security as Option).Underlying; - if (underlying.Price == 0) - { - var lastKnownPrice = _algorithm.GetLastKnownPrice(underlying.Symbol); - underlying.SetMarketPrice(lastKnownPrice); - if (underlying.Price == 0) - { - Log.Error($"BrokerageTransactionHandler.SyncOptionHoldings(): no price for {underlying.Symbol.Value}, {security.Symbol.Value} not synced."); - continue; - } - } - - Log.Trace($"BrokerageTransactionHandler.SyncOptionHoldings(): syncing option holding: Symbol: {security.Symbol.Value}, Holdings: {security.Holdings.Quantity}, Position: {position}"); - HandleOptionNotification(new OptionNotificationEventArgs(security.Symbol, position)); - } - } - - if (brokerageHoldings != null) - { - Log.Trace($"BrokerageTransactionHandler.SyncOptionHoldings(): Brokerage option holdings count {brokerageHoldings.Count}. Exit"); - } - } - catch (Exception err) - { - Log.Error(err, "BrokerageTransactionHandler.SyncOptionHoldings(): failed to sync the option holdings."); - } - } - /// /// Option notification event is received and new order events are generated /// diff --git a/Tests/Engine/BrokerageTransactionHandlerTests/BrokerageTransactionHandlerTests.cs b/Tests/Engine/BrokerageTransactionHandlerTests/BrokerageTransactionHandlerTests.cs index 0cf7897249d5..52657918e0fa 100644 --- a/Tests/Engine/BrokerageTransactionHandlerTests/BrokerageTransactionHandlerTests.cs +++ b/Tests/Engine/BrokerageTransactionHandlerTests/BrokerageTransactionHandlerTests.cs @@ -2298,9 +2298,9 @@ public void ProcessSynchronousEventsSkipsTheOptionHoldingsSyncForTheDayWhenTheBr } [TestCase(true, 1, 0, -600, Description = "Short Call -> ITM, not listed at the brokerage, the sync books the assignment")] - [TestCase(false, 0, -7, 100, Description = "Short Call -> ITM, not listed at the brokerage, the brokerage reports option position changes itself, nothing booked")] - public void ProcessSynchronousEventsSyncsTheOptionHoldingsOnlyWhenTheBrokerageEnablesIt( - bool holdingsSyncEnabled, + [TestCase(false, 0, -7, 100, Description = "Short Call -> ITM, not listed at the brokerage, the brokerage overrides the sync away, nothing booked")] + public void ProcessSynchronousEventsSkipsTheOptionHoldingsSyncWhenTheBrokerageOverridesIt( + bool performOptionHoldingsSync, int expectedTickets, int expectedOptionPosition, int expectedUnderlyingPosition @@ -2316,7 +2316,7 @@ int expectedUnderlyingPosition equity.SetMarketPrice(new Tick { Value = 455m }); algorithm.SetLiveMode(true); - using var brokerage = new NoSubmitTestBrokerage(algorithm, holdingsSyncEnabled); + using var brokerage = new NoSubmitTestBrokerage(algorithm, performOptionHoldingsSync); // the brokerage does not list the contract, as a stale holdings read does after a reconnect brokerage.AccountHoldings = new List { new Holding { Symbol = equity.Symbol, Quantity = 100 } }; _transactionHandler = new TestBrokerageTransactionHandler(); @@ -3498,12 +3498,23 @@ internal class NoSubmitTestBrokerage : Brokerage public override bool IsConnected => _underlyingBrokerage.IsConnected; - public override bool HoldingsSyncEnabled { get; } + // false stands for a brokerage that reports every option position change itself and overrides the sync away + private readonly bool _performOptionHoldingsSync; - public NoSubmitTestBrokerage(IAlgorithm algorithm, bool holdingsSyncEnabled = true) : base("NoSubmitTestBrokerage") + public NoSubmitTestBrokerage(IAlgorithm algorithm, bool performOptionHoldingsSync = true) : base("NoSubmitTestBrokerage") { _underlyingBrokerage = new BacktestingBrokerage(algorithm); - HoldingsSyncEnabled = holdingsSyncEnabled; + _performOptionHoldingsSync = performOptionHoldingsSync; + } + + public override bool PerformOptionHoldingsSync(IAlgorithm algorithm, DateTime currentTimeUtc) + { + if (!_performOptionHoldingsSync) + { + return true; + } + + return base.PerformOptionHoldingsSync(algorithm, currentTimeUtc); } public override bool PlaceOrder(Order order) {