diff --git a/Algorithm.CSharp/AddTwoAndRemoveOneOptionContractRegressionAlgorithm.cs b/Algorithm.CSharp/AddTwoAndRemoveOneOptionContractRegressionAlgorithm.cs index 7d1807b35b3c..b1001e555cb8 100644 --- a/Algorithm.CSharp/AddTwoAndRemoveOneOptionContractRegressionAlgorithm.cs +++ b/Algorithm.CSharp/AddTwoAndRemoveOneOptionContractRegressionAlgorithm.cs @@ -72,7 +72,10 @@ public override void OnData(Slice slice) throw new RegressionTestException("No configuration for underlying was found!"); } - if (!Portfolio.Invested) + if (!Portfolio.Invested && + // This security will be liquidated due to impending split, let's not trade it again after the contract is removed. + // Trying to trade it will make the security to be re-added + Securities[_contract2].IsTradable) { Buy(_contract2, 1); } diff --git a/Algorithm.CSharp/ResolutionSwitchingAlgorithm.cs b/Algorithm.CSharp/ResolutionSwitchingAlgorithm.cs index 7370ac840662..b2c12722c11f 100644 --- a/Algorithm.CSharp/ResolutionSwitchingAlgorithm.cs +++ b/Algorithm.CSharp/ResolutionSwitchingAlgorithm.cs @@ -59,7 +59,9 @@ public override void Initialize() /// Slice object keyed by symbol containing the stock data public override void OnData(Slice slice) { - if (!Portfolio.Invested) + if (!Portfolio.Invested && + // Wait for the security to be re-added in the OnSecuritiesChanged event before trying to trade it + Securities.TryGetValue(_spy, out var security) && security.IsTradable) { MarketOrder(_spy, 651); // QTY 651 is equal to `SetHoldings(_spy, 1)` Debug("Purchased Stock"); diff --git a/Algorithm.CSharp/StringToSymbolImplicitConversionRegressionAlgorithm.cs b/Algorithm.CSharp/StringToSymbolImplicitConversionRegressionAlgorithm.cs index 1a946d57aa87..f00a69870e43 100644 --- a/Algorithm.CSharp/StringToSymbolImplicitConversionRegressionAlgorithm.cs +++ b/Algorithm.CSharp/StringToSymbolImplicitConversionRegressionAlgorithm.cs @@ -13,10 +13,10 @@ * limitations under the License. */ -using System; using System.Collections.Generic; using QuantConnect.Data; using QuantConnect.Interfaces; +using QuantConnect.Orders; namespace QuantConnect.Algorithm.CSharp { @@ -43,16 +43,16 @@ public override void Initialize() /// Slice object keyed by symbol containing the stock data public override void OnData(Slice slice) { - try + var ticket = MarketOrder("PEPE", 1); + + if (ticket.Status != OrderStatus.Invalid) { - MarketOrder("PEPE", 1); + throw new RegressionTestException($"Expected order to be invalid since PEPE is not a valid ticker, but was {ticket.Status}"); } - catch (Exception exception) + + if (!Portfolio.Invested) { - if (exception.Message.Contains("PEPE was not found", StringComparison.InvariantCultureIgnoreCase) && !Portfolio.Invested) - { - SetHoldings("SPY", 1); - } + SetHoldings("SPY", 1); } } @@ -69,7 +69,7 @@ public override void OnData(Slice slice) /// /// Data Points count of all timeslices of algorithm /// - public long DataPoints => 1582; + public long DataPoints => 1583; /// /// Data Points count of the algorithm history diff --git a/Algorithm.CSharp/TradingNotAddedEquitiesRegressionAlgorithm.cs b/Algorithm.CSharp/TradingNotAddedEquitiesRegressionAlgorithm.cs new file mode 100644 index 000000000000..2791da71de2c --- /dev/null +++ b/Algorithm.CSharp/TradingNotAddedEquitiesRegressionAlgorithm.cs @@ -0,0 +1,139 @@ +/* + * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. + * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * +*/ + +using System.Collections.Generic; +using QuantConnect.Data; +using QuantConnect.Interfaces; +using QuantConnect.Orders; + +namespace QuantConnect.Algorithm.CSharp +{ + /// + /// Regression algorithm asserting that equities can be traded even if they are not added to the algorithm. + /// They will be automatically added as tradable securities an seeded when an order is placed for them. + /// + public class TradingNotAddedEquitiesRegressionAlgorithm : QCAlgorithm, IRegressionAlgorithmDefinition + { + private readonly Symbol _equitySymbol = QuantConnect.Symbol.Create("GOOG", SecurityType.Equity, Market.USA); + + public override void Initialize() + { + SetStartDate(2015, 12, 24); + SetEndDate(2015, 12, 24); + SetCash(1000000); + } + + protected void AssertSecurityIsAdded(Symbol symbol) + { + if (!Securities.TryGetValue(symbol, out var security) || ActiveSecurities.ContainsKey(symbol) || !security.IsTradable) + { + throw new RegressionTestException($"Contract {symbol} was not added as tradable security"); + } + } + + protected void AssertSecurityIsNotAdded(Symbol symbol) + { + if (Securities.TryGetValue(symbol, out var security) && ActiveSecurities.ContainsKey(symbol) && security.IsTradable) + { + throw new RegressionTestException($"Contract {symbol} was added as tradable security when it should not have been"); + } + } + + public override void OnData(Slice slice) + { + if (!Portfolio.Invested) + { + AssertSecurityIsNotAdded(_equitySymbol); + + var ticket = Buy(_equitySymbol, 1); + if (ticket.Status == OrderStatus.Invalid) + { + throw new RegressionTestException($"Order for {_equitySymbol} was rejected"); + } + + AssertSecurityIsAdded(_equitySymbol); + + // We are done + Quit(); + } + } + + public override void OnOrderEvent(OrderEvent orderEvent) + { + Log(orderEvent.ToString()); + } + + /// + /// This is used by the regression test system to indicate if the open source Lean repository has the required data to run this algorithm. + /// + public virtual bool CanRunLocally { get; } = true; + + /// + /// This is used by the regression test system to indicate which languages this algorithm is written in. + /// + public virtual List Languages { get; } = new() { Language.CSharp }; + + /// + /// Data Points count of all timeslices of algorithm + /// + public virtual long DataPoints => 11; + + /// + /// Data Points count of the algorithm history + /// + public virtual int AlgorithmHistoryDataPoints => 0; + + /// + /// Final status of the algorithm + /// + public virtual AlgorithmStatus AlgorithmStatus => AlgorithmStatus.Completed; + + /// + /// This is used by the regression test system to indicate what the expected statistics are from running the algorithm + /// + public virtual Dictionary ExpectedStatistics => new Dictionary + { + {"Total Orders", "0"}, + {"Average Win", "0%"}, + {"Average Loss", "0%"}, + {"Compounding Annual Return", "0%"}, + {"Drawdown", "0%"}, + {"Expectancy", "0"}, + {"Start Equity", "1000000"}, + {"End Equity", "1000000"}, + {"Net Profit", "0%"}, + {"Sharpe Ratio", "0"}, + {"Sortino Ratio", "0"}, + {"Probabilistic Sharpe Ratio", "0%"}, + {"Loss Rate", "0%"}, + {"Win Rate", "0%"}, + {"Profit-Loss Ratio", "0"}, + {"Alpha", "0"}, + {"Beta", "0"}, + {"Annual Standard Deviation", "0"}, + {"Annual Variance", "0"}, + {"Information Ratio", "0"}, + {"Tracking Error", "0"}, + {"Treynor Ratio", "0"}, + {"Total Fees", "$0.00"}, + {"Estimated Strategy Capacity", "$0"}, + {"Lowest Capacity Asset", ""}, + {"Portfolio Turnover", "0%"}, + {"Drawdown Recovery", "0"}, + {"OrderListHash", "d41d8cd98f00b204e9800998ecf8427e"} + }; + } +} diff --git a/Algorithm.CSharp/TradingNotAddedOptionsRegressionAlgorithm.cs b/Algorithm.CSharp/TradingNotAddedOptionsRegressionAlgorithm.cs new file mode 100644 index 000000000000..db93057f0f4f --- /dev/null +++ b/Algorithm.CSharp/TradingNotAddedOptionsRegressionAlgorithm.cs @@ -0,0 +1,206 @@ +/* + * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. + * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * +*/ + +using System; +using System.Collections.Generic; +using System.Linq; +using QuantConnect.Data; +using QuantConnect.Orders; +using QuantConnect.Securities.Option; + +namespace QuantConnect.Algorithm.CSharp +{ + /// + /// Regression algorithm asserting that options can be traded even if they are not added to the algorithm. + /// They will be automatically added as tradable securities an seeded when an order is placed for them. + /// + public class TradingNotAddedOptionsRegressionAlgorithm : TradingNotAddedEquitiesRegressionAlgorithm + { + private Symbol _optionSymbol; + private Symbol _deselectedContractSymbol; + private Symbol _notSelectedContractSymbol; + + public override void Initialize() + { + SetStartDate(2015, 12, 24); + SetEndDate(2015, 12, 28); + SetCash(1000000); + + var option = AddOption("GOOG"); + _optionSymbol = option.Symbol; + + // set our strike/expiry filter for this option chain + // SetFilter method accepts TimeSpan objects or integer for days. + // The following statements yield the same filtering criteria + option.SetFilter(u => u.StandardsOnly() + .Strikes(-2, +2) + .Expiration(7, 180) + .Contracts(contracts => + { + if (_deselectedContractSymbol == null) + { + var contractsList = contracts.ToList(); + _deselectedContractSymbol = contractsList.First(x => x.ID.OptionRight == OptionRight.Call && x.ID.StrikePrice == 750m && x.ID.Date.Date == new DateTime(2016, 06, 17)); + // This contract will never be selected so it's never added to the Securities collection + _notSelectedContractSymbol = contractsList.OrderByDescending(x => x.ID.Date).First(); + + return contractsList.Where(x => x != _notSelectedContractSymbol); + } + + // Filter out the contract we selected last time, we don't want it to be selected so it's marked as not tradable + return contracts.Where(x => x != _deselectedContractSymbol && x != _notSelectedContractSymbol); + })); + + // use the underlying equity as the benchmark + SetBenchmark("GOOG"); + } + + private void AssertTradeContract(Symbol symbol) + { + var ticket = Sell(symbol, 1); + if (ticket.Status == OrderStatus.Invalid) + { + throw new RegressionTestException($"Deselected contract {symbol} was not traded when it should have been"); + } + + AssertSecurityIsAdded(symbol); + + // Now let's remove it and try to trade it again, but in a strategy + RemoveSecurity(symbol); + AssertSecurityIsNotAdded(symbol); + + var strategy = OptionStrategies.Straddle(_optionSymbol, symbol.ID.StrikePrice, symbol.ID.Date); + if (strategy.OptionLegs.Count != 2 || + strategy.OptionLegs.Any(leg => leg.Symbol == null) || + !strategy.OptionLegs.Any(leg => leg.Symbol == symbol) || + !strategy.OptionLegs.Any(leg => leg.Symbol == symbol.GetMirrorOptionSymbol())) + { + throw new RegressionTestException("Option leg symbols were not set"); + } + + var tickets = Sell(strategy, 1); + if (tickets.Count == 0 || tickets.Any(x => x.Status == OrderStatus.Invalid)) + { + throw new RegressionTestException($"Deselected contract {symbol} was not traded as part of the strategy when it should have been"); + } + AssertSecurityIsAdded(symbol); + } + + public override void OnData(Slice slice) + { + if (Time.Day == 24) + { + if (_deselectedContractSymbol == null || _notSelectedContractSymbol == null) + { + throw new RegressionTestException("Trading contracts were not set"); + } + + if (!Securities.TryGetValue(_deselectedContractSymbol, out var deselectedContract)) + { + throw new RegressionTestException($"Deselected contract {_deselectedContractSymbol} is tradable"); + } + } + else if (Time.Day == 28 && !Portfolio.Invested) + { + if (slice.OptionChains.TryGetValue(_optionSymbol, out var chain)) + { + if (_deselectedContractSymbol == null || _notSelectedContractSymbol == null) + { + throw new RegressionTestException("Trading contracts were not set"); + } + + AssertSecurityIsNotAdded(_deselectedContractSymbol); + AssertSecurityIsNotAdded(_notSelectedContractSymbol); + + // Now we have _deselectedContractSymbol which was selected in a previous date but deselected for today. + // Let's trade it + AssertTradeContract(_deselectedContractSymbol); + + // Let's do the same with _notSelectedContractSymbol which was never selected + AssertTradeContract(_notSelectedContractSymbol); + + // We are done testing + Quit(); + } + } + } + + public override void OnOrderEvent(OrderEvent orderEvent) + { + Log(orderEvent.ToString()); + } + + /// + /// This is used by the regression test system to indicate if the open source Lean repository has the required data to run this algorithm. + /// + public override bool CanRunLocally { get; } = true; + + /// + /// This is used by the regression test system to indicate which languages this algorithm is written in. + /// + public override List Languages { get; } = new() { Language.CSharp }; + + /// + /// Data Points count of all timeslices of algorithm + /// + public override long DataPoints => 14642; + + /// + /// Data Points count of the algorithm history + /// + public override int AlgorithmHistoryDataPoints => 22; + + /// + /// Final status of the algorithm + /// + public override AlgorithmStatus AlgorithmStatus => AlgorithmStatus.Completed; + + /// + /// This is used by the regression test system to indicate what the expected statistics are from running the algorithm + /// + public override Dictionary ExpectedStatistics => new Dictionary + { + {"Total Orders", "8"}, + {"Average Win", "0%"}, + {"Average Loss", "-0.02%"}, + {"Compounding Annual Return", "-5.436%"}, + {"Drawdown", "0.100%"}, + {"Expectancy", "-1"}, + {"Start Equity", "1000000"}, + {"End Equity", "999327"}, + {"Net Profit", "-0.067%"}, + {"Sharpe Ratio", "-9.644"}, + {"Sortino Ratio", "0"}, + {"Probabilistic Sharpe Ratio", "1.216%"}, + {"Loss Rate", "100%"}, + {"Win Rate", "0%"}, + {"Profit-Loss Ratio", "0"}, + {"Alpha", "0"}, + {"Beta", "0"}, + {"Annual Standard Deviation", "0.005"}, + {"Annual Variance", "0"}, + {"Information Ratio", "-7.772"}, + {"Tracking Error", "0.005"}, + {"Treynor Ratio", "0"}, + {"Total Fees", "$8.00"}, + {"Estimated Strategy Capacity", "$0"}, + {"Lowest Capacity Asset", "GOOCV WBGM92QHN8ZQ|GOOCV VP83T1ZUHROL"}, + {"Portfolio Turnover", "0.86%"}, + {"Drawdown Recovery", "0"}, + {"OrderListHash", "fc224d8177907f98e3381d2d58067b48"} + }; + } +} diff --git a/Algorithm.Python/StringToSymbolImplicitConversionRegressionAlgorithm.py b/Algorithm.Python/StringToSymbolImplicitConversionRegressionAlgorithm.py index fe23b230a046..307cb7c57a8a 100644 --- a/Algorithm.Python/StringToSymbolImplicitConversionRegressionAlgorithm.py +++ b/Algorithm.Python/StringToSymbolImplicitConversionRegressionAlgorithm.py @@ -31,8 +31,10 @@ def on_data(self, data): Arguments: data: Slice object keyed by symbol containing the stock data ''' - try: - self.market_order("PEPE", 1) - except Exception as exception: - if "PEPE was not found" in str(exception) and not self.portfolio.invested: - self.set_holdings("SPY", 1) + ticket = self.market_order("PEPE", 1) + + if ticket.status != OrderStatus.Invalid: + raise Exception(f"Expected order to be invalid since PEPE is not a valid ticker, but was {ticket.status}") + + if not self.portfolio.invested: + self.set_holdings("SPY", 1) diff --git a/Algorithm/QCAlgorithm.Trading.cs b/Algorithm/QCAlgorithm.Trading.cs index f61496471dbf..2956504f09b9 100644 --- a/Algorithm/QCAlgorithm.Trading.cs +++ b/Algorithm/QCAlgorithm.Trading.cs @@ -13,16 +13,17 @@ * limitations under the License. */ -using System; -using System.Linq; -using QuantConnect.Orders; +using QuantConnect.Algorithm.Framework.Portfolio; using QuantConnect.Interfaces; +using QuantConnect.Orders; +using QuantConnect.Orders.TimeInForces; using QuantConnect.Securities; -using System.Collections.Generic; +using QuantConnect.Securities.Future; using QuantConnect.Securities.Option; +using System; +using System.Collections.Generic; +using System.Linq; using static QuantConnect.StringExtensions; -using QuantConnect.Algorithm.Framework.Portfolio; -using QuantConnect.Orders.TimeInForces; namespace QuantConnect.Algorithm { @@ -237,7 +238,8 @@ public OrderTicket MarketOrder(Symbol symbol, double quantity, bool asynchronous [DocumentationAttribute(TradingAndOrders)] public OrderTicket MarketOrder(Symbol symbol, decimal quantity, bool asynchronous = false, string tag = "", IOrderProperties orderProperties = null) { - var security = Securities[symbol]; + var security = GetSecurityForOrder(symbol); + // check the exchange is open before sending a market order, if it's not open then convert it into a market on open order. // For futures and FOPs, market orders can be submitted on extended hours, so we let them through. if ((security.Type != SecurityType.Future && security.Type != SecurityType.FutureOption) && !security.Exchange.ExchangeOpen) @@ -314,7 +316,7 @@ public OrderTicket MarketOnOpenOrder(Symbol symbol, decimal quantity, bool async var properties = orderProperties ?? DefaultOrderProperties?.Clone(); InvalidateGoodTilDateTimeInForce(properties); - var security = Securities[symbol]; + var security = GetSecurityForOrder(symbol); var request = CreateSubmitOrderRequest(OrderType.MarketOnOpen, security, quantity, tag, properties, asynchronous); return SubmitOrderRequest(request); @@ -365,7 +367,7 @@ public OrderTicket MarketOnCloseOrder(Symbol symbol, decimal quantity, bool asyn var properties = orderProperties ?? DefaultOrderProperties?.Clone(); InvalidateGoodTilDateTimeInForce(properties); - var security = Securities[symbol]; + var security = GetSecurityForOrder(symbol); var request = CreateSubmitOrderRequest(OrderType.MarketOnClose, security, quantity, tag, properties, asynchronous); return SubmitOrderRequest(request); @@ -416,7 +418,7 @@ public OrderTicket LimitOrder(Symbol symbol, double quantity, decimal limitPrice [DocumentationAttribute(TradingAndOrders)] public OrderTicket LimitOrder(Symbol symbol, decimal quantity, decimal limitPrice, bool asynchronous = false, string tag = "", IOrderProperties orderProperties = null) { - var security = Securities[symbol]; + var security = GetSecurityForOrder(symbol); var request = CreateSubmitOrderRequest(OrderType.Limit, security, quantity, tag, orderProperties ?? DefaultOrderProperties?.Clone(), asynchronous, limitPrice: limitPrice); @@ -468,7 +470,7 @@ public OrderTicket StopMarketOrder(Symbol symbol, double quantity, decimal stopP [DocumentationAttribute(TradingAndOrders)] public OrderTicket StopMarketOrder(Symbol symbol, decimal quantity, decimal stopPrice, bool asynchronous = false, string tag = "", IOrderProperties orderProperties = null) { - var security = Securities[symbol]; + var security = GetSecurityForOrder(symbol); var request = CreateSubmitOrderRequest(OrderType.StopMarket, security, quantity, tag, orderProperties ?? DefaultOrderProperties?.Clone(), asynchronous, stopPrice: stopPrice); @@ -529,7 +531,7 @@ public OrderTicket TrailingStopOrder(Symbol symbol, double quantity, decimal tra public OrderTicket TrailingStopOrder(Symbol symbol, decimal quantity, decimal trailingAmount, bool trailingAsPercentage, bool asynchronous = false, string tag = "", IOrderProperties orderProperties = null) { - var security = Securities[symbol]; + var security = GetSecurityForOrder(symbol); var stopPrice = Orders.TrailingStopOrder.CalculateStopPrice(security.Price, trailingAmount, trailingAsPercentage, quantity > 0 ? OrderDirection.Buy : OrderDirection.Sell); return TrailingStopOrder(symbol, quantity, stopPrice, trailingAmount, trailingAsPercentage, asynchronous, tag, orderProperties); @@ -589,7 +591,7 @@ public OrderTicket TrailingStopOrder(Symbol symbol, double quantity, decimal sto public OrderTicket TrailingStopOrder(Symbol symbol, decimal quantity, decimal stopPrice, decimal trailingAmount, bool trailingAsPercentage, bool asynchronous = false, string tag = "", IOrderProperties orderProperties = null) { - var security = Securities[symbol]; + var security = GetSecurityForOrder(symbol); var request = CreateSubmitOrderRequest( OrderType.TrailingStop, security, @@ -655,7 +657,7 @@ public OrderTicket StopLimitOrder(Symbol symbol, double quantity, decimal stopPr public OrderTicket StopLimitOrder(Symbol symbol, decimal quantity, decimal stopPrice, decimal limitPrice, bool asynchronous = false, string tag = "", IOrderProperties orderProperties = null) { - var security = Securities[symbol]; + var security = GetSecurityForOrder(symbol); var request = CreateSubmitOrderRequest(OrderType.StopLimit, security, quantity, tag, stopPrice: stopPrice, limitPrice: limitPrice, properties: orderProperties ?? DefaultOrderProperties?.Clone(), asynchronous: asynchronous); @@ -713,7 +715,7 @@ public OrderTicket LimitIfTouchedOrder(Symbol symbol, double quantity, decimal t public OrderTicket LimitIfTouchedOrder(Symbol symbol, decimal quantity, decimal triggerPrice, decimal limitPrice, bool asynchronous = false, string tag = "", IOrderProperties orderProperties = null) { - var security = Securities[symbol]; + var security = GetSecurityForOrder(symbol); var request = CreateSubmitOrderRequest(OrderType.LimitIfTouched, security, quantity, tag, triggerPrice: triggerPrice, limitPrice: limitPrice, properties: orderProperties ?? DefaultOrderProperties?.Clone(), asynchronous: asynchronous); @@ -733,7 +735,7 @@ public OrderTicket LimitIfTouchedOrder(Symbol symbol, decimal quantity, decimal [DocumentationAttribute(TradingAndOrders)] public OrderTicket ExerciseOption(Symbol optionSymbol, int quantity, bool asynchronous = false, string tag = "", IOrderProperties orderProperties = null) { - var option = (Option)Securities[optionSymbol]; + var option = (Option)GetSecurityForOrder(optionSymbol); // SubmitOrderRequest.Quantity indicates the change in holdings quantity, therefore manual exercise quantities must be negative // PreOrderChecksImpl confirms that we don't hold a short position, so we're lenient here and accept +/- quantity values @@ -902,10 +904,7 @@ private List SubmitComboOrder(List legs, decimal quantity, dec List submitRequests = new(capacity: legs.Count); foreach (var leg in legs) { - if (!Securities.TryGetValue(leg.Symbol, out var security)) - { - throw new InvalidOperationException(Invariant($"Couldn't find one of the strategy's securities in the algorithm securities list. {leg}")); - } + var security = GetSecurityForOrder(leg.Symbol); if (leg.OrderPrice.HasValue) { @@ -1205,6 +1204,49 @@ private OrderResponse PreOrderChecksImpl(SubmitOrderRequest request) return OrderResponse.Success(request); } + /// + /// Gets the security for the given symbol. + /// This method is intended to get a security that is going to be traded, so it will try to + /// add the security if it's not found in the algorithm's securities collection and it meets + /// the requirements to be added (e.g. not delisted, not expired, etc). + /// The added security will be seeded with data so that it can be traded immediately. + /// + private Security GetSecurityForOrder(Symbol symbol) + { + var isCanonical = symbol.IsCanonical(); + if (Securities.TryGetValue(symbol, out var security) && + // Let canonical and delisted securities through instead of throwing. An invalid ticket will be returned later on when trying to submit the order. + (isCanonical || security.IsTradable || security.IsDelisted)) + { + return security; + } + + if (security == null || !security.IsTradable) + { + // Try to add and seed the security, but don't is it's a canonical symbol + if (!isCanonical && + // Indexes are not tradable by default + symbol.SecurityType != SecurityType.Index && + (!symbol.HasUnderlying || + (symbol.SecurityType.IsOption() && !OptionSymbol.IsOptionContractExpired(symbol, UtcTime)) || + (symbol.SecurityType == SecurityType.Future && !FuturesExpiryUtilityFunctions.IsFutureContractExpired(symbol, UtcTime, MarketHoursDatabase)))) + { + // Send one time warning + security = AddSecurity(symbol); + + if (!Settings.SeedInitialPrices) + { + AlgorithmUtils.SeedSecurities([security], this); + } + + return security; + } + } + + throw new InvalidOperationException($"The symbol {symbol} is not found in the algorithm's securities collection " + + "and cannot be re-added due to it being delisted or no longer tradable."); + } + /// /// Liquidate your portfolio holdings /// diff --git a/Common/Securities/Future/FuturesExpiryUtilityFunctions.cs b/Common/Securities/Future/FuturesExpiryUtilityFunctions.cs index 5d47f6608ecf..52f568b93389 100644 --- a/Common/Securities/Future/FuturesExpiryUtilityFunctions.cs +++ b/Common/Securities/Future/FuturesExpiryUtilityFunctions.cs @@ -25,6 +25,8 @@ namespace QuantConnect.Securities.Future /// public static class FuturesExpiryUtilityFunctions { + private static readonly MarketHoursDatabase MarketHoursDatabase = MarketHoursDatabase.FromDataFolder(); + /// /// Get holiday list from the MHDB given the market and the symbol of the security /// @@ -115,7 +117,7 @@ public static DateTime NthLastBusinessDay(DateTime time, int n, IEnumerable x.Date); - if(n > daysInMonth) + if (n > daysInMonth) { throw new ArgumentOutOfRangeException(nameof(n), Invariant( $"Number of days ({n}) is larger than the size of month({daysInMonth})" @@ -382,6 +384,21 @@ public static DateTime ThirdFriday(DateTime time, Symbol contract) return AddBusinessDaysIfHoliday(thirdFriday, -1, holidays); } + /// + /// Checks if the future contract is expired. + /// + public static bool IsFutureContractExpired(Symbol symbol, DateTime currentUtcTime, MarketHoursDatabase marketHoursDatabase = null) + { + var exchangeHours = (marketHoursDatabase ?? MarketHoursDatabase).GetExchangeHours(symbol.ID.Market, symbol, symbol.SecurityType); + var currentTimeInExchangeTz = currentUtcTime.ConvertFromUtc(exchangeHours.TimeZone); + if (currentTimeInExchangeTz >= symbol.ID.Date) + { + return true; + } + + return false; + } + private static readonly Dictionary ExpiriesPriorMonth = new Dictionary { { Futures.Energy.ArgusLLSvsWTIArgusTradeMonth, 1 },