Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 77 additions & 8 deletions Brokerages/Brokerage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -509,13 +510,6 @@ protected virtual List<CashAmount> GetCashBalance(Dictionary<string, string> bro
/// </summary>
public virtual bool AccountInstantlyUpdated => false;

/// <summary>
/// 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
/// </summary>
public virtual bool HoldingsSyncEnabled => true;

/// <summary>
/// Cache holding the legs of a combo order until all of them have been placed, so the brokerage can submit them together
/// </summary>
Expand Down Expand Up @@ -589,7 +583,7 @@ public virtual bool ShouldPerformCashSync(DateTime currentTimeUtc)
}

/// <summary>
/// Synchronizes the cashbook with the brokerage account
/// Synchronizes the cashbook, and the option holdings, with the brokerage account
/// </summary>
/// <param name="algorithm">The algorithm instance</param>
/// <param name="currentTimeUtc">The current time (UTC)</param>
Expand All @@ -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<CashAmount> balances = null;
Expand Down Expand Up @@ -698,6 +694,79 @@ public virtual bool PerformCashSync(IAlgorithm algorithm, DateTime currentTimeUt
return true;
}

/// <summary>
/// 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
/// </summary>
/// <param name="algorithm">The algorithm instance</param>
/// <param name="currentTimeUtc">The current time (UTC)</param>
/// <returns>True if the option holdings sync was performed successfully</returns>
public virtual bool PerformOptionHoldingsSync(IAlgorithm algorithm, DateTime currentTimeUtc)
{
try
{
var brokerageHoldings = default(Dictionary<Symbol, decimal>);
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
Expand Down
71 changes: 0 additions & 71 deletions Engine/TransactionHandlers/BrokerageTransactionHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -1734,76 +1733,6 @@ private void HandleDelistingNotification(DelistingNotificationEventArgs e)
}
}

/// <summary>
/// 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
/// </summary>
private void SyncOptionHoldings()
{
if (_brokerage is Brokerage { HoldingsSyncEnabled: false })
{
return;
}

try
{
var brokerageHoldings = default(Dictionary<Symbol, decimal>);
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.");
}
}

/// <summary>
/// Option notification event is received and new order events are generated
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<Holding> { new Holding { Symbol = equity.Symbol, Quantity = 100 } };
_transactionHandler = new TestBrokerageTransactionHandler();
Expand Down Expand Up @@ -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)
{
Expand Down
Loading