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);