From 7b33c450d59c7ab059cf6cb28519440d7d54ec7b Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Tue, 11 Aug 2026 19:26:47 -0400 Subject: [PATCH] Fail loudly on canonical continuous future orders and add Future.Canonical - PortfolioTarget.Percent rejects canonical future symbols with an instructive error, so CalculateOrderQuantity returns 0 with a clear message and SetHoldings no longer submits doomed orders - Non-tradable order rejections for canonical symbols now point to Future.Mapped and explain it is not set until after Initialize - Order methods throw a named ArgumentNullException for null symbols (common case: ordering Future.Mapped from Initialize) instead of NRE - Add Future.Canonical as an alias of Symbol.Canonical - Warn once when a future stop/limit/trigger price deviates more than 10% from the contract market price while the continuous subscription uses a non-Raw data normalization mode --- ...utureCanonicalOrdersRegressionAlgorithm.cs | 192 ++++++++++++++++++ ...utureCanonicalOrdersRegressionAlgorithm.py | 85 ++++++++ Algorithm/QCAlgorithm.Trading.cs | 46 ++++- .../Framework/Portfolio/PortfolioTarget.cs | 10 + .../Messages.Algorithm.Framework.Portfolio.cs | 9 + Common/Messages/Messages.Algorithm.cs | 27 +++ Common/Securities/Future/Future.cs | 9 + Tests/Algorithm/AlgorithmTradingTests.cs | 73 +++++++ .../Portfolio/PortfolioTargetTests.cs | 17 ++ .../Portfolio/SignalExportTargetTests.cs | 6 +- 10 files changed, 472 insertions(+), 2 deletions(-) create mode 100644 Algorithm.CSharp/ContinuousFutureCanonicalOrdersRegressionAlgorithm.cs create mode 100644 Algorithm.Python/ContinuousFutureCanonicalOrdersRegressionAlgorithm.py diff --git a/Algorithm.CSharp/ContinuousFutureCanonicalOrdersRegressionAlgorithm.cs b/Algorithm.CSharp/ContinuousFutureCanonicalOrdersRegressionAlgorithm.cs new file mode 100644 index 000000000000..20ef95fd1be9 --- /dev/null +++ b/Algorithm.CSharp/ContinuousFutureCanonicalOrdersRegressionAlgorithm.cs @@ -0,0 +1,192 @@ +/* + * 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 QuantConnect.Data; +using QuantConnect.Orders; +using QuantConnect.Interfaces; +using QuantConnect.Securities; +using System.Collections.Generic; +using QuantConnect.Securities.Future; + +namespace QuantConnect.Algorithm.CSharp +{ + /// + /// Regression algorithm asserting the canonical continuous future symbol cannot be traded directly and fails loudly, + /// while its currently mapped contract can: returns + /// zero with an instructive error, + /// submits no orders and direct orders produce an invalid ticket pointing to . + /// Also asserts and that is null until the continuous + /// contract universe makes its first selection, after Initialize. + /// + public class ContinuousFutureCanonicalOrdersRegressionAlgorithm : QCAlgorithm, IRegressionAlgorithmDefinition + { + private Future _continuousContract; + private bool _canonicalChecksDone; + private bool _traded; + + public override void Initialize() + { + SetStartDate(2013, 10, 7); + SetEndDate(2013, 10, 10); + + _continuousContract = AddFuture(Futures.Indices.SP500EMini, + dataNormalizationMode: DataNormalizationMode.BackwardsRatio, + dataMappingMode: DataMappingMode.OpenInterest, + contractDepthOffset: 0 + ); + + if (_continuousContract.Mapped != null) + { + throw new RegressionTestException("Expected Future.Mapped to be null during Initialize: " + + "the continuous contract universe does not make its first selection until after Initialize"); + } + + if (_continuousContract.Canonical != _continuousContract.Symbol) + { + throw new RegressionTestException("Expected Future.Canonical to be the continuous contract symbol itself"); + } + } + + public override void OnData(Slice slice) + { + if (_continuousContract.Mapped == null || !slice.Bars.ContainsKey(_continuousContract.Symbol)) + { + return; + } + + if (!_canonicalChecksDone) + { + _canonicalChecksDone = true; + var canonical = _continuousContract.Symbol; + + // Continuous contract data is keyed by the canonical symbol + if (slice.Bars[canonical].Symbol != canonical) + { + throw new RegressionTestException("Expected the continuous contract bar to be keyed by the canonical symbol"); + } + + // The canonical symbol is not tradable: no order quantity can be computed for it + if (CalculateOrderQuantity(canonical, 1m) != 0) + { + throw new RegressionTestException("Expected CalculateOrderQuantity to return 0 for the canonical symbol"); + } + + // SetHoldings must not submit orders for the canonical symbol + if (SetHoldings(canonical, 0.5).Count != 0 || Portfolio.Invested) + { + throw new RegressionTestException("Expected SetHoldings to not submit orders for the canonical symbol"); + } + + // Direct orders on the canonical symbol are rejected with an instructive message + var ticket = MarketOrder(canonical, 1); + if (ticket.Status != OrderStatus.Invalid) + { + throw new RegressionTestException("Expected a market order on the canonical symbol to be invalid"); + } + if (!ticket.SubmitRequest.Response.ErrorMessage.Contains("canonical")) + { + throw new RegressionTestException("Expected the invalid canonical order error message to explain " + + $"the symbol is canonical, but was: '{ticket.SubmitRequest.Response.ErrorMessage}'"); + } + } + + if (!_traded) + { + _traded = true; + + // The currently mapped contract is the tradable one + var ticket = MarketOrder(_continuousContract.Mapped, 1); + if (ticket.Status == OrderStatus.Invalid) + { + throw new RegressionTestException("Expected a market order on the mapped contract to be valid"); + } + } + } + + public override void OnEndOfAlgorithm() + { + if (!_canonicalChecksDone) + { + throw new RegressionTestException("No data was received so the canonical symbol checks were not performed"); + } + + if (!Portfolio.Invested) + { + throw new RegressionTestException("Expected to hold a position in the mapped contract"); + } + } + + /// + /// 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 bool CanRunLocally { get; } = true; + + /// + /// This is used by the regression test system to indicate which languages this algorithm is written in. + /// + public List Languages { get; } = new() { Language.CSharp, Language.Python }; + + /// + /// Data Points count of all timeslices of algorithm + /// + public long DataPoints => 10881; + + /// + /// Data Points count of the algorithm history + /// + public int AlgorithmHistoryDataPoints => 0; + + /// + /// Final status of the algorithm + /// + public AlgorithmStatus AlgorithmStatus => AlgorithmStatus.Completed; + + /// + /// This is used by the regression test system to indicate what the expected statistics are from running the algorithm + /// + public Dictionary ExpectedStatistics => new Dictionary + { + {"Total Orders", "1"}, + {"Average Win", "0%"}, + {"Average Loss", "0%"}, + {"Compounding Annual Return", "79.914%"}, + {"Drawdown", "1.900%"}, + {"Expectancy", "0"}, + {"Start Equity", "100000"}, + {"End Equity", "100645.7"}, + {"Net Profit", "0.646%"}, + {"Sharpe Ratio", "3.958"}, + {"Sortino Ratio", "0"}, + {"Probabilistic Sharpe Ratio", "0%"}, + {"Loss Rate", "0%"}, + {"Win Rate", "0%"}, + {"Profit-Loss Ratio", "0"}, + {"Alpha", "-0.372"}, + {"Beta", "0.815"}, + {"Annual Standard Deviation", "0.222"}, + {"Annual Variance", "0.049"}, + {"Information Ratio", "-12.526"}, + {"Tracking Error", "0.052"}, + {"Treynor Ratio", "1.077"}, + {"Total Fees", "$2.15"}, + {"Estimated Strategy Capacity", "$2800000000.00"}, + {"Lowest Capacity Asset", "ES VMKLFZIH2MTD"}, + {"Portfolio Turnover", "20.89%"}, + {"Drawdown Recovery", "3"}, + {"OrderListHash", "2338180a2a964389525a9f1221f97a06"} + }; + } +} diff --git a/Algorithm.Python/ContinuousFutureCanonicalOrdersRegressionAlgorithm.py b/Algorithm.Python/ContinuousFutureCanonicalOrdersRegressionAlgorithm.py new file mode 100644 index 000000000000..1089834f2fed --- /dev/null +++ b/Algorithm.Python/ContinuousFutureCanonicalOrdersRegressionAlgorithm.py @@ -0,0 +1,85 @@ +# 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. + +from AlgorithmImports import * + +### +### Regression algorithm asserting the canonical continuous future symbol cannot be traded directly and fails loudly, +### while its currently mapped contract can: calculate_order_quantity returns zero with an instructive error, +### set_holdings submits no orders and direct orders produce an invalid ticket pointing to future.mapped. +### Also asserts future.canonical and that future.mapped is None until the continuous contract universe makes +### its first selection, after initialize. +### +class ContinuousFutureCanonicalOrdersRegressionAlgorithm(QCAlgorithm): + + def initialize(self): + self.set_start_date(2013, 10, 7) + self.set_end_date(2013, 10, 10) + + self._continuous_contract = self.add_future(Futures.Indices.SP_500_E_MINI, + data_normalization_mode=DataNormalizationMode.BACKWARDS_RATIO, + data_mapping_mode=DataMappingMode.OPEN_INTEREST, + contract_depth_offset=0) + + if self._continuous_contract.mapped is not None: + raise AssertionError("Expected future.mapped to be None during initialize: " + "the continuous contract universe does not make its first selection until after initialize") + + if self._continuous_contract.canonical != self._continuous_contract.symbol: + raise AssertionError("Expected future.canonical to be the continuous contract symbol itself") + + self._canonical_checks_done = False + self._traded = False + + def on_data(self, slice): + if self._continuous_contract.mapped is None or not slice.bars.contains_key(self._continuous_contract.symbol): + return + + if not self._canonical_checks_done: + self._canonical_checks_done = True + canonical = self._continuous_contract.symbol + + # Continuous contract data is keyed by the canonical symbol, and the future object itself can be used as the key + if slice.bars.get(canonical) is None or slice.bars.get(self._continuous_contract) is None: + raise AssertionError("Expected the continuous contract bar to be accessible through the canonical symbol and the future object") + + # The canonical symbol is not tradable: no order quantity can be computed for it + if self.calculate_order_quantity(canonical, 1.0) != 0: + raise AssertionError("Expected calculate_order_quantity to return 0 for the canonical symbol") + + # set_holdings must not submit orders for the canonical symbol + if len(self.set_holdings(canonical, 0.5)) != 0 or self.portfolio.invested: + raise AssertionError("Expected set_holdings to not submit orders for the canonical symbol") + + # Direct orders on the canonical symbol are rejected with an instructive message + ticket = self.market_order(canonical, 1) + if ticket.status != OrderStatus.INVALID: + raise AssertionError("Expected a market order on the canonical symbol to be invalid") + if "canonical" not in ticket.submit_request.response.error_message: + raise AssertionError("Expected the invalid canonical order error message to explain the symbol is canonical, " + f"but was: '{ticket.submit_request.response.error_message}'") + + if not self._traded: + self._traded = True + + # The currently mapped contract is the tradable one + ticket = self.market_order(self._continuous_contract.mapped, 1) + if ticket.status == OrderStatus.INVALID: + raise AssertionError("Expected a market order on the mapped contract to be valid") + + def on_end_of_algorithm(self): + if not self._canonical_checks_done: + raise AssertionError("No data was received so the canonical symbol checks were not performed") + + if not self.portfolio.invested: + raise AssertionError("Expected to hold a position in the mapped contract") diff --git a/Algorithm/QCAlgorithm.Trading.cs b/Algorithm/QCAlgorithm.Trading.cs index ddba02f0249c..5fb393e1acd7 100644 --- a/Algorithm/QCAlgorithm.Trading.cs +++ b/Algorithm/QCAlgorithm.Trading.cs @@ -34,6 +34,7 @@ public partial class QCAlgorithm private bool _isDailyResolutionMarketOrderConversionWarningSent; private bool _isMarketOnOpenOrderRestrictedForFuturesWarningSent; private bool _isGtdTfiForMooAndMocOrdersValidationWarningSent; + private bool _isFutureOrderPriceFarFromMarketPriceWarningSent; private bool _isOptionsOrderOnStockSplitWarningSent; private bool _liquidateSymbolNotFoundWarningSent; @@ -1079,8 +1080,12 @@ private OrderResponse PreOrderChecksImpl(SubmitOrderRequest request) if (!security.IsTradable) { + // Canonical symbols (e.g. the continuous futures contract) are never tradable: + // point the user to the mapped contract instead of just rejecting the order return OrderResponse.Error(request, OrderResponseErrorCode.NonTradableSecurity, - $"The security with symbol '{request.Symbol}' is marked as non-tradable." + security.Symbol.IsCanonical() + ? Messages.QCAlgorithm.CanonicalSymbolNotTradable(security.Symbol) + : $"The security with symbol '{request.Symbol}' is marked as non-tradable." ); } @@ -1113,6 +1118,38 @@ private OrderResponse PreOrderChecksImpl(SubmitOrderRequest request) return OrderResponse.Error(request, OrderResponseErrorCode.SecurityPriceZero, request.Symbol.GetZeroPriceMessage()); } + // Futures continuous contract prices are adjusted unless DataNormalizationMode.Raw is used, so stop/limit prices + // computed from them can sit far away from the raw prices the mapped contract actually trades at, producing orders + // that fill immediately or never. Warn once when an order price deviates >10% from the contract's market price. + if (!_isFutureOrderPriceFarFromMarketPriceWarningSent && security.Type == SecurityType.Future) + { + var maxDeviation = 0m; + foreach (var orderPrice in new[] { request.StopPrice, request.LimitPrice, request.TriggerPrice }) + { + if (orderPrice != 0) + { + maxDeviation = Math.Max(maxDeviation, Math.Abs(orderPrice - price) / price); + } + } + + if (maxDeviation > 0.1m) + { + var normalizationMode = SubscriptionManager.SubscriptionDataConfigService + .GetSubscriptionDataConfigs(request.Symbol.Canonical) + .Select(x => x.DataNormalizationMode) + .FirstOrDefault(x => x != DataNormalizationMode.Raw, DataNormalizationMode.Raw); + + if (normalizationMode != DataNormalizationMode.Raw) + { + _isFutureOrderPriceFarFromMarketPriceWarningSent = true; + Debug($"Warning: The {request.OrderType} order price(s) for '{request.Symbol.Value}' deviate more than 10% from its market price ({price.SmartRounding()}). " + + $"The continuous contract '{request.Symbol.Canonical}' uses DataNormalizationMode.{normalizationMode}, whose adjusted prices can differ significantly " + + "from the raw prices the mapped contract trades at. If the order price was computed from continuous contract data, use the mapped contract's " + + "price instead (Securities[future.Mapped].Price) or add the future with DataNormalizationMode.Raw."); + } + } + } + // check quote currency existence/conversion rate on all orders var quoteCurrency = security.QuoteCurrency.Symbol; if (!Portfolio.CashBook.TryGetValue(quoteCurrency, out var quoteCash)) @@ -1268,6 +1305,13 @@ private OrderResponse PreOrderChecksImpl(SubmitOrderRequest request) /// private Security GetSecurityForOrder(Symbol symbol) { + if (symbol == null) + { + // A common source of null symbols is accessing Future.Mapped from Initialize, before the first mapping. + // Explain that instead of letting an NRE bubble up. See Messages.QCAlgorithm.OrderSymbolNull + throw new ArgumentNullException(nameof(symbol), Messages.QCAlgorithm.OrderSymbolNull()); + } + 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. diff --git a/Common/Algorithm/Framework/Portfolio/PortfolioTarget.cs b/Common/Algorithm/Framework/Portfolio/PortfolioTarget.cs index b9e37f257c43..22a02aacfdf2 100644 --- a/Common/Algorithm/Framework/Portfolio/PortfolioTarget.cs +++ b/Common/Algorithm/Framework/Portfolio/PortfolioTarget.cs @@ -153,6 +153,16 @@ public static IPortfolioTarget Percent(IAlgorithm algorithm, Symbol symbol, deci return null; } + // The canonical continuous futures contract is not tradable, so instead of producing a quantity that will + // only generate an invalid order, fail loudly here. Continuous contract data gives the canonical security + // a non-zero price, so without this check a plausible-looking quantity would be silently computed. + // Other canonical symbols (options) have no price and are already rejected by the zero-price check below. + if (security.Symbol.IsCanonical() && security.Symbol.SecurityType == SecurityType.Future) + { + algorithm.Error(Messages.PortfolioTarget.UnableToComputeOrderQuantityForCanonicalSymbol(security.Symbol)); + return null; + } + if (security.Price == 0) { algorithm.Error(symbol.GetZeroPriceMessage()); diff --git a/Common/Messages/Messages.Algorithm.Framework.Portfolio.cs b/Common/Messages/Messages.Algorithm.Framework.Portfolio.cs index 8c40abb2af5c..14f03e808b63 100644 --- a/Common/Messages/Messages.Algorithm.Framework.Portfolio.cs +++ b/Common/Messages/Messages.Algorithm.Framework.Portfolio.cs @@ -63,6 +63,15 @@ public static string UnableToComputeOrderQuantityDueToNullResult(QuantConnect.Sy return Invariant($"Unable to compute order quantity of {symbol}. Reason: {result.Reason} Returning null."); } + /// + /// Returns a string message saying an order quantity cannot be computed for the given canonical symbol + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string UnableToComputeOrderQuantityForCanonicalSymbol(QuantConnect.Symbol symbol) + { + return Invariant($"Unable to compute an order quantity for '{symbol}'. {QCAlgorithm.CanonicalSymbolNotTradable(symbol)}"); + } + /// /// Parses the given portfolio target into a string message containing basic information about it /// diff --git a/Common/Messages/Messages.Algorithm.cs b/Common/Messages/Messages.Algorithm.cs index 288a0197cbcc..937b7bb6af1c 100644 --- a/Common/Messages/Messages.Algorithm.cs +++ b/Common/Messages/Messages.Algorithm.cs @@ -90,6 +90,33 @@ public static string SetWarmupAlreadyInitialized() return $"{AlgorithmPrefix()}.{FormatCode("SetWarmup")}(): This method cannot be used after algorithm initialized"; } + /// + /// Returns a string message saying the given canonical symbol is not tradable, with guidance + /// on what to trade instead + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string CanonicalSymbolNotTradable(QuantConnect.Symbol symbol) + { + var guidance = symbol.SecurityType == SecurityType.Future + ? $"trade the currently mapped contract instead, accessible through the '{FormatCode("Mapped")}' property of the future " + + $"security returned by {AlgorithmPrefix()}.{FormatCode("AddFuture")}(). Note it is not set until after {FormatCode("Initialize")}, " + + "once the continuous contract universe makes its first selection" + : "select a specific contract from the chain instead"; + return $"The symbol '{symbol}' is a canonical symbol and is not tradable; {guidance}."; + } + + /// + /// Returns a string message for order methods receiving a null symbol, explaining the common cause: + /// accessing before the first continuous contract mapping + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string OrderSymbolNull() + { + return $"The order symbol is null. If it comes from the '{FormatCode("Mapped")}' property of a future, note it is not set " + + $"until after {FormatCode("Initialize")}, once the continuous contract universe makes its first selection; " + + $"place orders from {FormatCode("OnData")}, {FormatCode("OnSecuritiesChanged")} or scheduled events instead."; + } + /// /// Returns a string message saying the first argument to AddData must be a custom data class /// diff --git a/Common/Securities/Future/Future.cs b/Common/Securities/Future/Future.cs index 227284bf35eb..d634cc142259 100644 --- a/Common/Securities/Future/Future.cs +++ b/Common/Securities/Future/Future.cs @@ -175,11 +175,20 @@ public SettlementType SettlementType /// /// Gets or sets the currently mapped symbol for the security /// + /// Null until the continuous contract universe performs its first selection, which happens + /// on algorithm start, after Initialize. Use it from OnData, OnSecuritiesChanged + /// or scheduled events instead of from Initialize public Symbol Mapped { get; set; } + /// + /// Gets the canonical symbol of the future, that is, the continuous contract symbol returned by + /// AddFuture. For the continuous contract security itself this is its own symbol. + /// + public Symbol Canonical => Symbol.Canonical; + /// /// Gets or sets the contract filter /// diff --git a/Tests/Algorithm/AlgorithmTradingTests.cs b/Tests/Algorithm/AlgorithmTradingTests.cs index 831a9702e59a..bf8315edfc24 100644 --- a/Tests/Algorithm/AlgorithmTradingTests.cs +++ b/Tests/Algorithm/AlgorithmTradingTests.cs @@ -1656,6 +1656,79 @@ public void MarketOnOpenOrdersNotSupportedForFutures() Assert.That(ticket, Has.Property("Status").EqualTo(OrderStatus.Invalid)); } + [Test] + public void OrdersOnCanonicalFutureSymbolAreInvalidWithInstructiveMessage() + { + var algo = GetAlgorithm(out _, 1, 0); + var future = algo.AddFuture(Futures.Indices.SP500EMini); + Update(future, 100); + + // Future.Canonical is an alias of the canonical symbol + Assert.AreEqual(future.Symbol, future.Canonical); + + var ticket = algo.MarketOrder(future.Symbol, 1); + + Assert.AreEqual(OrderStatus.Invalid, ticket.Status); + Assert.AreEqual(OrderResponseErrorCode.NonTradableSecurity, ticket.SubmitRequest.Response.ErrorCode); + Assert.That(ticket.SubmitRequest.Response.ErrorMessage, Does.Contain("canonical")); + Assert.That(ticket.SubmitRequest.Response.ErrorMessage, Does.Contain("mapped").IgnoreCase); + } + + [Test] + public void CalculateOrderQuantityOnCanonicalFutureSymbolReturnsZeroWithError() + { + var algo = GetAlgorithm(out _, 1, 0); + var future = algo.AddFuture(Futures.Indices.SP500EMini); + Update(future, 100); + + var quantity = algo.CalculateOrderQuantity(future.Symbol, 1m); + + Assert.AreEqual(0, quantity); + Assert.IsTrue(algo.ErrorMessages.Any(x => x.Contains("canonical")), + "Expected an error message explaining the canonical symbol is not tradable"); + + // SetHoldings must not submit any order for the canonical symbol + var tickets = algo.SetHoldings(future.Symbol, 0.5m); + Assert.IsEmpty(tickets); + } + + [Test] + public void OrderWithNullSymbolThrowsWithMappedContractGuidance() + { + var algo = GetAlgorithm(out _, 1, 0); + + // e.g. ordering Future.Mapped before the first continuous contract mapping resolved it + var exception = Assert.Throws(() => algo.MarketOrder((Symbol)null, 1)); + + Assert.That(exception.Message, Does.Contain("mapped").IgnoreCase); + } + + [TestCase(true)] + [TestCase(false)] + public void WarnsWhenFutureStopOrLimitPriceIsFarFromMarketPriceUnderAdjustedNormalization(bool rawNormalization) + { + var algo = GetAlgorithm(out _, 1, 0); + algo.AddFuture(Futures.Indices.SP500EMini, + dataNormalizationMode: rawNormalization ? DataNormalizationMode.Raw : DataNormalizationMode.BackwardsRatio); + var es20h20 = algo.AddFutureContract( + QuantConnect.Symbol.CreateFuture(Futures.Indices.SP500EMini, Market.CME, new DateTime(2020, 3, 20))); + Update(es20h20, 100); + + // stop price 50% away from the contract's market price + algo.StopMarketOrder(es20h20.Symbol, -1, 50m); + + var expectedWarnings = rawNormalization ? 0 : 1; + Assert.AreEqual(expectedWarnings, algo.DebugMessages.Count(x => x.Contains("DataNormalizationMode"))); + + // the warning is only sent once + algo.StopMarketOrder(es20h20.Symbol, -1, 45m); + Assert.AreEqual(expectedWarnings, algo.DebugMessages.Count(x => x.Contains("DataNormalizationMode"))); + + // orders with prices close to the market price don't warn + algo.StopMarketOrder(es20h20.Symbol, -1, 95m); + Assert.AreEqual(expectedWarnings, algo.DebugMessages.Count(x => x.Contains("DataNormalizationMode"))); + } + [Test] public void OptionOrdersAreNotAllowedDuringASplit() { diff --git a/Tests/Algorithm/Framework/Portfolio/PortfolioTargetTests.cs b/Tests/Algorithm/Framework/Portfolio/PortfolioTargetTests.cs index edaddc202d70..db1b1f95f69b 100644 --- a/Tests/Algorithm/Framework/Portfolio/PortfolioTargetTests.cs +++ b/Tests/Algorithm/Framework/Portfolio/PortfolioTargetTests.cs @@ -20,6 +20,7 @@ using QuantConnect.Data.Market; using QuantConnect.Securities; using QuantConnect.Tests.Engine; +using QuantConnect.Tests.Engine.DataFeeds; namespace QuantConnect.Tests.Algorithm.Framework.Portfolio { @@ -94,6 +95,22 @@ public void PercentReturnsNullIfBuyingPowerModelError() Assert.IsNull(target); } + [Test] + public void PercentReturnsNullForCanonicalFutureSymbol() + { + var algorithm = new AlgorithmStub(); + algorithm.SetFinishedWarmingUp(); + var future = algorithm.AddFuture(Futures.Indices.SP500EMini); + // continuous contract data gives the canonical security a price, but it is still not tradable + future.SetMarketPrice(new Tick { Value = 100m }); + + var target = PortfolioTarget.Percent(algorithm, future.Symbol, 1m); + + Assert.IsNull(target); + Assert.IsTrue(algorithm.ErrorMessages.Any(x => x.Contains("canonical")), + "Expected an error message explaining the canonical symbol is not tradable"); + } + [TestCase(-3, true)] [TestCase(3, true)] [TestCase(2, false)] diff --git a/Tests/Algorithm/Framework/Portfolio/SignalExportTargetTests.cs b/Tests/Algorithm/Framework/Portfolio/SignalExportTargetTests.cs index 181ff7f3ab02..31e412e4feea 100644 --- a/Tests/Algorithm/Framework/Portfolio/SignalExportTargetTests.cs +++ b/Tests/Algorithm/Framework/Portfolio/SignalExportTargetTests.cs @@ -322,7 +322,11 @@ public void SignalExportManagerGetsCorrectPortfolioTargetArray(SecurityType secu algorithm.SetFinishedWarmingUp(); algorithm.SetCash(100000); - var security = algorithm.AddSecurity(securityType, ticker); + // Canonical future symbols are not tradable and cannot have holdings nor a target quantity computed + // for them, so we use a specific contract + var security = securityType == SecurityType.Future + ? algorithm.AddFutureContract(Symbol.CreateFuture(ticker, Market.CME, new DateTime(2022, 3, 18))) + : algorithm.AddSecurity(securityType, ticker); security.SetMarketPrice(new Tick(new DateTime(2022, 01, 04), security.Symbol, 144.80m, 144.82m)); security.Holdings.SetHoldings(144.81m, quantity);