From 0c0487e07c77c2707e2a1ff62d8aa219e585e710 Mon Sep 17 00:00:00 2001 From: Josue Nina Date: Mon, 9 Mar 2026 15:56:30 -0500 Subject: [PATCH 1/7] Add ContractMultiplier setter to Cfd via CfdSymbolProperties --- Common/Securities/Cfd/Cfd.cs | 11 ++++- Common/Securities/Cfd/CfdSymbolProperties.cs | 51 ++++++++++++++++++++ Tests/Common/Securities/Cfd/CfdTests.cs | 21 ++++++-- 3 files changed, 76 insertions(+), 7 deletions(-) create mode 100644 Common/Securities/Cfd/CfdSymbolProperties.cs diff --git a/Common/Securities/Cfd/Cfd.cs b/Common/Securities/Cfd/Cfd.cs index 2bf86cac0d2e..d6d8015b1443 100644 --- a/Common/Securities/Cfd/Cfd.cs +++ b/Common/Securities/Cfd/Cfd.cs @@ -62,6 +62,8 @@ public Cfd(SecurityExchangeHours exchangeHours, Securities.MarginInterestRateModel.Null ) { + _symbolProperties = new CfdSymbolProperties(symbolProperties); + SymbolProperties = _symbolProperties; Holdings = new CfdHolding(this, currencyConverter); } @@ -102,15 +104,20 @@ public Cfd(Symbol symbol, Securities.MarginInterestRateModel.Null ) { + _symbolProperties = new CfdSymbolProperties(symbolProperties); + SymbolProperties = _symbolProperties; Holdings = new CfdHolding(this, currencyConverter); } + private readonly CfdSymbolProperties _symbolProperties; + /// - /// Gets the contract multiplier for this CFD security + /// Gets or sets the contract multiplier for this CFD security /// public decimal ContractMultiplier { - get { return SymbolProperties.ContractMultiplier; } + get { return _symbolProperties.ContractMultiplier; } + set { _symbolProperties.SetContractMultiplier(value); } } /// diff --git a/Common/Securities/Cfd/CfdSymbolProperties.cs b/Common/Securities/Cfd/CfdSymbolProperties.cs new file mode 100644 index 000000000000..59d300dfd790 --- /dev/null +++ b/Common/Securities/Cfd/CfdSymbolProperties.cs @@ -0,0 +1,51 @@ +/* + * 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. +*/ + +namespace QuantConnect.Securities.Cfd +{ + /// + /// Represents common properties for a specific CFD contract + /// + public class CfdSymbolProperties : SymbolProperties + { + /// + /// The contract multiplier for the security. + /// + /// + /// If manually set by a consumer, this value will be used instead of the + /// and also allows to make + /// sure it is not overridden when the symbol properties database gets updated. + /// + private decimal? _contractMultiplier; + + /// + /// The contract multiplier for the security + /// + public override decimal ContractMultiplier => _contractMultiplier ?? base.ContractMultiplier; + + /// + /// Creates an instance of the class from + /// + public CfdSymbolProperties(SymbolProperties properties) + : base(properties) + { + } + + internal void SetContractMultiplier(decimal multiplier) + { + _contractMultiplier = multiplier; + } + } +} diff --git a/Tests/Common/Securities/Cfd/CfdTests.cs b/Tests/Common/Securities/Cfd/CfdTests.cs index 37c749b59569..7835955b0f24 100644 --- a/Tests/Common/Securities/Cfd/CfdTests.cs +++ b/Tests/Common/Securities/Cfd/CfdTests.cs @@ -13,7 +13,6 @@ * limitations under the License. */ -using System; using NUnit.Framework; using QuantConnect.Data; using QuantConnect.Data.Market; @@ -27,12 +26,24 @@ public class CfdTests [Test] public void ConstructorExtractsQuoteCurrency() { - var symbol = Symbol.Create("DE30EUR", SecurityType.Cfd, Market.Oanda); - var config = new SubscriptionDataConfig(typeof(TradeBar), symbol, Resolution.Minute, TimeZones.Utc, TimeZones.NewYork, true, true, true); - var symbolProperties = new SymbolProperties("Dax German index", "EUR", 1, 1, 1, string.Empty); - var cfd = new QuantConnect.Securities.Cfd.Cfd(SecurityExchangeHours.AlwaysOpen(config.DataTimeZone), new Cash("EUR", 0, 0), config, symbolProperties, ErrorCurrencyConverter.Instance, RegisteredSecurityDataTypesProvider.Null); + var cfd = CreateCfd(contractMultiplier: 1m); Assert.AreEqual("EUR", cfd.QuoteCurrency.Symbol); } + [Test] + public void ContractMultiplierCanBeSetByUser() + { + var cfd = CreateCfd(contractMultiplier: 1m); + cfd.ContractMultiplier = 5m; + Assert.AreEqual(5m, cfd.ContractMultiplier); + } + + private static QuantConnect.Securities.Cfd.Cfd CreateCfd(decimal contractMultiplier) + { + var symbol = Symbol.Create("DE30EUR", SecurityType.Cfd, Market.Oanda); + var config = new SubscriptionDataConfig(typeof(TradeBar), symbol, Resolution.Minute, TimeZones.Utc, TimeZones.NewYork, true, true, true); + var symbolProperties = new SymbolProperties("Dax German index", "EUR", contractMultiplier, 1, 1, string.Empty); + return new QuantConnect.Securities.Cfd.Cfd(SecurityExchangeHours.AlwaysOpen(config.DataTimeZone), new Cash("EUR", 0, 0), config, symbolProperties, ErrorCurrencyConverter.Instance, RegisteredSecurityDataTypesProvider.Null); + } } } From 6b988f337a729eff8b29b2a568e289c088d61ef5 Mon Sep 17 00:00:00 2001 From: Josue Nina Date: Tue, 10 Mar 2026 23:11:23 -0500 Subject: [PATCH 2/7] Fix regression tests --- ...SymbolPropertiesDatabaseEntriesAlgorithm.cs | 18 ++++++++++++++++-- ...SymbolPropertiesDatabaseEntriesAlgorithm.py | 15 +++++++++++++-- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/Algorithm.CSharp/ManuallySetMarketHoursAndSymbolPropertiesDatabaseEntriesAlgorithm.cs b/Algorithm.CSharp/ManuallySetMarketHoursAndSymbolPropertiesDatabaseEntriesAlgorithm.cs index 81217a605941..8417c366655d 100644 --- a/Algorithm.CSharp/ManuallySetMarketHoursAndSymbolPropertiesDatabaseEntriesAlgorithm.cs +++ b/Algorithm.CSharp/ManuallySetMarketHoursAndSymbolPropertiesDatabaseEntriesAlgorithm.cs @@ -16,6 +16,7 @@ using System; using System.Collections.Generic; using QuantConnect.Interfaces; +using QuantConnect.Securities; namespace QuantConnect.Algorithm.CSharp { @@ -54,7 +55,7 @@ public override void Initialize() throw new RegressionTestException("Expected the SPY CFD market hours to be the same as the underlying equity market hours."); } - if (!ReferenceEquals(spyCfd.SymbolProperties, equitySymbolProperties)) + if (!SymbolPropertiesAreEquivalent(spyCfd.SymbolProperties, equitySymbolProperties)) { throw new RegressionTestException("Expected the SPY CFD symbol properties to be the same as the underlying equity symbol properties."); } @@ -74,12 +75,25 @@ public override void Initialize() throw new RegressionTestException("Expected the AUDUSD CFD market hours to be the same as the underlying forex market hours."); } - if (!ReferenceEquals(audUsdCfd.SymbolProperties, audUsdForexSymbolProperties)) + if (!SymbolPropertiesAreEquivalent(audUsdCfd.SymbolProperties, audUsdForexSymbolProperties)) { throw new RegressionTestException("Expected the AUDUSD CFD symbol properties to be the same as the underlying forex symbol properties."); } } + private static bool SymbolPropertiesAreEquivalent(SymbolProperties a, SymbolProperties b) + { + return a.Description == b.Description && + a.QuoteCurrency == b.QuoteCurrency && + a.ContractMultiplier == b.ContractMultiplier && + a.MinimumPriceVariation == b.MinimumPriceVariation && + a.LotSize == b.LotSize && + a.MarketTicker == b.MarketTicker && + a.MinimumOrderSize == b.MinimumOrderSize && + a.PriceMagnifier == b.PriceMagnifier && + a.StrikeMultiplier == b.StrikeMultiplier; + } + /// /// This is used by the regression test system to indicate if the open source Lean repository has the required data to run this algorithm. /// diff --git a/Algorithm.Python/ManuallySetMarketHoursAndSymbolPropertiesDatabaseEntriesAlgorithm.py b/Algorithm.Python/ManuallySetMarketHoursAndSymbolPropertiesDatabaseEntriesAlgorithm.py index 254d49bfb643..2d8869466331 100644 --- a/Algorithm.Python/ManuallySetMarketHoursAndSymbolPropertiesDatabaseEntriesAlgorithm.py +++ b/Algorithm.Python/ManuallySetMarketHoursAndSymbolPropertiesDatabaseEntriesAlgorithm.py @@ -51,7 +51,7 @@ def initialize(self): if json.JsonConvert.serialize_object(spy_cfd.exchange.hours) != json.JsonConvert.serialize_object(equity_market_hours_entry.exchange_hours): raise AssertionError("Expected the SPY CFD market hours to be the same as the underlying equity market hours.") - if json.JsonConvert.serialize_object(spy_cfd.symbol_properties) != json.JsonConvert.serialize_object(equity_symbol_properties): + if not self.symbol_properties_are_equivalent(spy_cfd.symbol_properties, equity_symbol_properties): raise AssertionError("Expected the SPY CFD symbol properties to be the same as the underlying equity symbol properties.") # We can also do it for a specific ticker @@ -66,5 +66,16 @@ def initialize(self): if json.JsonConvert.serialize_object(aud_usd_cfd.exchange.hours) != json.JsonConvert.serialize_object(aud_usd_forex_market_hours_entry.exchange_hours): raise AssertionError("Expected the AUDUSD CFD market hours to be the same as the underlying forex market hours.") - if json.JsonConvert.serialize_object(aud_usd_cfd.symbol_properties) != json.JsonConvert.serialize_object(aud_usd_forex_symbol_properties): + if not self.symbol_properties_are_equivalent(aud_usd_cfd.symbol_properties, aud_usd_forex_symbol_properties): raise AssertionError("Expected the AUDUSD CFD symbol properties to be the same as the underlying forex symbol properties.") + + def symbol_properties_are_equivalent(self, a, b): + return (a.description == b.description and + a.quote_currency == b.quote_currency and + a.contract_multiplier == b.contract_multiplier and + a.minimum_price_variation == b.minimum_price_variation and + a.lot_size == b.lot_size and + a.market_ticker == b.market_ticker and + a.minimum_order_size == b.minimum_order_size and + a.price_magnifier == b.price_magnifier and + a.strike_multiplier == b.strike_multiplier) From 92bdd3c2b9bdc408de58d0e1b037aea29c5dedfd Mon Sep 17 00:00:00 2001 From: Josue Nina Date: Thu, 12 Mar 2026 10:00:10 -0500 Subject: [PATCH 3/7] Solve review comments --- ...ymbolPropertiesDatabaseEntriesAlgorithm.cs | 18 +------ ...ymbolPropertiesDatabaseEntriesAlgorithm.py | 15 +----- Common/Securities/Cfd/Cfd.cs | 10 +--- Common/Securities/Cfd/CfdSymbolProperties.cs | 51 ------------------- .../Option/OptionSymbolProperties.cs | 20 -------- Common/Securities/SymbolProperties.cs | 12 ++++- 6 files changed, 17 insertions(+), 109 deletions(-) delete mode 100644 Common/Securities/Cfd/CfdSymbolProperties.cs diff --git a/Algorithm.CSharp/ManuallySetMarketHoursAndSymbolPropertiesDatabaseEntriesAlgorithm.cs b/Algorithm.CSharp/ManuallySetMarketHoursAndSymbolPropertiesDatabaseEntriesAlgorithm.cs index 8417c366655d..81217a605941 100644 --- a/Algorithm.CSharp/ManuallySetMarketHoursAndSymbolPropertiesDatabaseEntriesAlgorithm.cs +++ b/Algorithm.CSharp/ManuallySetMarketHoursAndSymbolPropertiesDatabaseEntriesAlgorithm.cs @@ -16,7 +16,6 @@ using System; using System.Collections.Generic; using QuantConnect.Interfaces; -using QuantConnect.Securities; namespace QuantConnect.Algorithm.CSharp { @@ -55,7 +54,7 @@ public override void Initialize() throw new RegressionTestException("Expected the SPY CFD market hours to be the same as the underlying equity market hours."); } - if (!SymbolPropertiesAreEquivalent(spyCfd.SymbolProperties, equitySymbolProperties)) + if (!ReferenceEquals(spyCfd.SymbolProperties, equitySymbolProperties)) { throw new RegressionTestException("Expected the SPY CFD symbol properties to be the same as the underlying equity symbol properties."); } @@ -75,25 +74,12 @@ public override void Initialize() throw new RegressionTestException("Expected the AUDUSD CFD market hours to be the same as the underlying forex market hours."); } - if (!SymbolPropertiesAreEquivalent(audUsdCfd.SymbolProperties, audUsdForexSymbolProperties)) + if (!ReferenceEquals(audUsdCfd.SymbolProperties, audUsdForexSymbolProperties)) { throw new RegressionTestException("Expected the AUDUSD CFD symbol properties to be the same as the underlying forex symbol properties."); } } - private static bool SymbolPropertiesAreEquivalent(SymbolProperties a, SymbolProperties b) - { - return a.Description == b.Description && - a.QuoteCurrency == b.QuoteCurrency && - a.ContractMultiplier == b.ContractMultiplier && - a.MinimumPriceVariation == b.MinimumPriceVariation && - a.LotSize == b.LotSize && - a.MarketTicker == b.MarketTicker && - a.MinimumOrderSize == b.MinimumOrderSize && - a.PriceMagnifier == b.PriceMagnifier && - a.StrikeMultiplier == b.StrikeMultiplier; - } - /// /// This is used by the regression test system to indicate if the open source Lean repository has the required data to run this algorithm. /// diff --git a/Algorithm.Python/ManuallySetMarketHoursAndSymbolPropertiesDatabaseEntriesAlgorithm.py b/Algorithm.Python/ManuallySetMarketHoursAndSymbolPropertiesDatabaseEntriesAlgorithm.py index 2d8869466331..254d49bfb643 100644 --- a/Algorithm.Python/ManuallySetMarketHoursAndSymbolPropertiesDatabaseEntriesAlgorithm.py +++ b/Algorithm.Python/ManuallySetMarketHoursAndSymbolPropertiesDatabaseEntriesAlgorithm.py @@ -51,7 +51,7 @@ def initialize(self): if json.JsonConvert.serialize_object(spy_cfd.exchange.hours) != json.JsonConvert.serialize_object(equity_market_hours_entry.exchange_hours): raise AssertionError("Expected the SPY CFD market hours to be the same as the underlying equity market hours.") - if not self.symbol_properties_are_equivalent(spy_cfd.symbol_properties, equity_symbol_properties): + if json.JsonConvert.serialize_object(spy_cfd.symbol_properties) != json.JsonConvert.serialize_object(equity_symbol_properties): raise AssertionError("Expected the SPY CFD symbol properties to be the same as the underlying equity symbol properties.") # We can also do it for a specific ticker @@ -66,16 +66,5 @@ def initialize(self): if json.JsonConvert.serialize_object(aud_usd_cfd.exchange.hours) != json.JsonConvert.serialize_object(aud_usd_forex_market_hours_entry.exchange_hours): raise AssertionError("Expected the AUDUSD CFD market hours to be the same as the underlying forex market hours.") - if not self.symbol_properties_are_equivalent(aud_usd_cfd.symbol_properties, aud_usd_forex_symbol_properties): + if json.JsonConvert.serialize_object(aud_usd_cfd.symbol_properties) != json.JsonConvert.serialize_object(aud_usd_forex_symbol_properties): raise AssertionError("Expected the AUDUSD CFD symbol properties to be the same as the underlying forex symbol properties.") - - def symbol_properties_are_equivalent(self, a, b): - return (a.description == b.description and - a.quote_currency == b.quote_currency and - a.contract_multiplier == b.contract_multiplier and - a.minimum_price_variation == b.minimum_price_variation and - a.lot_size == b.lot_size and - a.market_ticker == b.market_ticker and - a.minimum_order_size == b.minimum_order_size and - a.price_magnifier == b.price_magnifier and - a.strike_multiplier == b.strike_multiplier) diff --git a/Common/Securities/Cfd/Cfd.cs b/Common/Securities/Cfd/Cfd.cs index d6d8015b1443..517070fef37d 100644 --- a/Common/Securities/Cfd/Cfd.cs +++ b/Common/Securities/Cfd/Cfd.cs @@ -62,8 +62,6 @@ public Cfd(SecurityExchangeHours exchangeHours, Securities.MarginInterestRateModel.Null ) { - _symbolProperties = new CfdSymbolProperties(symbolProperties); - SymbolProperties = _symbolProperties; Holdings = new CfdHolding(this, currencyConverter); } @@ -104,20 +102,16 @@ public Cfd(Symbol symbol, Securities.MarginInterestRateModel.Null ) { - _symbolProperties = new CfdSymbolProperties(symbolProperties); - SymbolProperties = _symbolProperties; Holdings = new CfdHolding(this, currencyConverter); } - private readonly CfdSymbolProperties _symbolProperties; - /// /// Gets or sets the contract multiplier for this CFD security /// public decimal ContractMultiplier { - get { return _symbolProperties.ContractMultiplier; } - set { _symbolProperties.SetContractMultiplier(value); } + get { return SymbolProperties.ContractMultiplier; } + set { SymbolProperties.SetContractMultiplier(value); } } /// diff --git a/Common/Securities/Cfd/CfdSymbolProperties.cs b/Common/Securities/Cfd/CfdSymbolProperties.cs deleted file mode 100644 index 59d300dfd790..000000000000 --- a/Common/Securities/Cfd/CfdSymbolProperties.cs +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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. -*/ - -namespace QuantConnect.Securities.Cfd -{ - /// - /// Represents common properties for a specific CFD contract - /// - public class CfdSymbolProperties : SymbolProperties - { - /// - /// The contract multiplier for the security. - /// - /// - /// If manually set by a consumer, this value will be used instead of the - /// and also allows to make - /// sure it is not overridden when the symbol properties database gets updated. - /// - private decimal? _contractMultiplier; - - /// - /// The contract multiplier for the security - /// - public override decimal ContractMultiplier => _contractMultiplier ?? base.ContractMultiplier; - - /// - /// Creates an instance of the class from - /// - public CfdSymbolProperties(SymbolProperties properties) - : base(properties) - { - } - - internal void SetContractMultiplier(decimal multiplier) - { - _contractMultiplier = multiplier; - } - } -} diff --git a/Common/Securities/Option/OptionSymbolProperties.cs b/Common/Securities/Option/OptionSymbolProperties.cs index c01176a9f63a..03f23cd242dd 100644 --- a/Common/Securities/Option/OptionSymbolProperties.cs +++ b/Common/Securities/Option/OptionSymbolProperties.cs @@ -20,21 +20,6 @@ namespace QuantConnect.Securities.Option /// public class OptionSymbolProperties : SymbolProperties { - /// - /// The contract multiplier for the security. - /// - /// - /// If manually set by a consumer, this value will be used instead of the - /// and also allows to make - /// sure it is not overridden when the symbol properties database gets updated. - /// - private decimal? _contractMultiplier; - - /// - /// The contract multiplier for the security - /// - public override decimal ContractMultiplier => _contractMultiplier ?? base.ContractMultiplier; - /// /// When the holder of an equity option exercises one contract, or when the writer of an equity option is assigned /// an exercise notice on one contract, this unit of trade, usually 100 shares of the underlying security, changes hands. @@ -65,10 +50,5 @@ internal void SetContractUnitOfTrade(int unitOfTrade) { ContractUnitOfTrade = unitOfTrade; } - - internal void SetContractMultiplier(decimal multiplier) - { - _contractMultiplier = multiplier; - } } } diff --git a/Common/Securities/SymbolProperties.cs b/Common/Securities/SymbolProperties.cs index 9c3db7e38b65..d33dfb426d26 100644 --- a/Common/Securities/SymbolProperties.cs +++ b/Common/Securities/SymbolProperties.cs @@ -31,6 +31,8 @@ public class SymbolProperties /// private SymbolPropertiesHolder _properties; + private decimal? _contractMultiplier; + /// /// The description of the security /// @@ -46,10 +48,18 @@ public class SymbolProperties /// public virtual decimal ContractMultiplier { - get => _properties.ContractMultiplier; + get => _contractMultiplier ?? _properties.ContractMultiplier; internal set => _properties.ContractMultiplier = value; } + /// + /// Sets a custom contract multiplier that persists through symbol properties database updates + /// + internal void SetContractMultiplier(decimal multiplier) + { + _contractMultiplier = multiplier; + } + /// /// The minimum price variation (tick size) for the security /// From a46192a51dd10bb7e03376b2c4e26ab0cc9369de Mon Sep 17 00:00:00 2001 From: Josue Nina Date: Mon, 16 Mar 2026 11:20:13 -0500 Subject: [PATCH 4/7] Address new review comments --- Common/Securities/Cfd/Cfd.cs | 12 +++-- Common/Securities/Cfd/CfdSymbolProperties.cs | 51 +++++++++++++++++++ .../Option/OptionSymbolProperties.cs | 20 ++++++++ Common/Securities/SymbolProperties.cs | 12 +---- 4 files changed, 80 insertions(+), 15 deletions(-) create mode 100644 Common/Securities/Cfd/CfdSymbolProperties.cs diff --git a/Common/Securities/Cfd/Cfd.cs b/Common/Securities/Cfd/Cfd.cs index 517070fef37d..2f74f5e04903 100644 --- a/Common/Securities/Cfd/Cfd.cs +++ b/Common/Securities/Cfd/Cfd.cs @@ -45,7 +45,7 @@ public Cfd(SecurityExchangeHours exchangeHours, IRegisteredSecurityDataTypesProvider registeredTypes) : base(config, quoteCurrency, - symbolProperties, + new CfdSymbolProperties(symbolProperties), new CfdExchange(exchangeHours), new CfdCache(), new SecurityPortfolioModel(), @@ -63,6 +63,7 @@ public Cfd(SecurityExchangeHours exchangeHours, ) { Holdings = new CfdHolding(this, currencyConverter); + _symbolProperties = (CfdSymbolProperties)SymbolProperties; } /// @@ -85,7 +86,7 @@ public Cfd(Symbol symbol, SecurityCache securityCache) : base(symbol, quoteCurrency, - symbolProperties, + new CfdSymbolProperties(symbolProperties), new CfdExchange(exchangeHours), securityCache, new SecurityPortfolioModel(), @@ -103,15 +104,18 @@ public Cfd(Symbol symbol, ) { Holdings = new CfdHolding(this, currencyConverter); + _symbolProperties = (CfdSymbolProperties)SymbolProperties; } + private readonly CfdSymbolProperties _symbolProperties; + /// /// Gets or sets the contract multiplier for this CFD security /// public decimal ContractMultiplier { - get { return SymbolProperties.ContractMultiplier; } - set { SymbolProperties.SetContractMultiplier(value); } + get { return _symbolProperties.ContractMultiplier; } + set { _symbolProperties.SetContractMultiplier(value); } } /// diff --git a/Common/Securities/Cfd/CfdSymbolProperties.cs b/Common/Securities/Cfd/CfdSymbolProperties.cs new file mode 100644 index 000000000000..10c15b2560dd --- /dev/null +++ b/Common/Securities/Cfd/CfdSymbolProperties.cs @@ -0,0 +1,51 @@ +/* + * 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. +*/ + +namespace QuantConnect.Securities.Cfd +{ + /// + /// Represents common properties for a specific CFD contract + /// + public class CfdSymbolProperties : SymbolProperties + { + /// + /// The contract multiplier for the security. + /// + /// + /// If manually set by a consumer, this value will be used instead of the + /// and also allows to make + /// sure it is not overridden when the symbol properties database gets updated. + /// + private decimal? _contractMultiplier; + + /// + /// The contract multiplier for the security + /// + public override decimal ContractMultiplier => _contractMultiplier ?? base.ContractMultiplier; + + /// + /// Creates an instance of the class from class + /// + public CfdSymbolProperties(SymbolProperties properties) + : base(properties) + { + } + + internal void SetContractMultiplier(decimal multiplier) + { + _contractMultiplier = multiplier; + } + } +} diff --git a/Common/Securities/Option/OptionSymbolProperties.cs b/Common/Securities/Option/OptionSymbolProperties.cs index 03f23cd242dd..c01176a9f63a 100644 --- a/Common/Securities/Option/OptionSymbolProperties.cs +++ b/Common/Securities/Option/OptionSymbolProperties.cs @@ -20,6 +20,21 @@ namespace QuantConnect.Securities.Option /// public class OptionSymbolProperties : SymbolProperties { + /// + /// The contract multiplier for the security. + /// + /// + /// If manually set by a consumer, this value will be used instead of the + /// and also allows to make + /// sure it is not overridden when the symbol properties database gets updated. + /// + private decimal? _contractMultiplier; + + /// + /// The contract multiplier for the security + /// + public override decimal ContractMultiplier => _contractMultiplier ?? base.ContractMultiplier; + /// /// When the holder of an equity option exercises one contract, or when the writer of an equity option is assigned /// an exercise notice on one contract, this unit of trade, usually 100 shares of the underlying security, changes hands. @@ -50,5 +65,10 @@ internal void SetContractUnitOfTrade(int unitOfTrade) { ContractUnitOfTrade = unitOfTrade; } + + internal void SetContractMultiplier(decimal multiplier) + { + _contractMultiplier = multiplier; + } } } diff --git a/Common/Securities/SymbolProperties.cs b/Common/Securities/SymbolProperties.cs index d33dfb426d26..9c3db7e38b65 100644 --- a/Common/Securities/SymbolProperties.cs +++ b/Common/Securities/SymbolProperties.cs @@ -31,8 +31,6 @@ public class SymbolProperties /// private SymbolPropertiesHolder _properties; - private decimal? _contractMultiplier; - /// /// The description of the security /// @@ -48,18 +46,10 @@ public class SymbolProperties /// public virtual decimal ContractMultiplier { - get => _contractMultiplier ?? _properties.ContractMultiplier; + get => _properties.ContractMultiplier; internal set => _properties.ContractMultiplier = value; } - /// - /// Sets a custom contract multiplier that persists through symbol properties database updates - /// - internal void SetContractMultiplier(decimal multiplier) - { - _contractMultiplier = multiplier; - } - /// /// The minimum price variation (tick size) for the security /// From f2d695f2edf16fb6b4b652b9cb842f5bd30bf6e6 Mon Sep 17 00:00:00 2001 From: Josue Nina Date: Mon, 16 Mar 2026 11:48:17 -0500 Subject: [PATCH 5/7] Fix regression tests --- ...SymbolPropertiesDatabaseEntriesAlgorithm.cs | 18 ++++++++++++++++-- ...SymbolPropertiesDatabaseEntriesAlgorithm.py | 15 +++++++++++++-- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/Algorithm.CSharp/ManuallySetMarketHoursAndSymbolPropertiesDatabaseEntriesAlgorithm.cs b/Algorithm.CSharp/ManuallySetMarketHoursAndSymbolPropertiesDatabaseEntriesAlgorithm.cs index 81217a605941..8417c366655d 100644 --- a/Algorithm.CSharp/ManuallySetMarketHoursAndSymbolPropertiesDatabaseEntriesAlgorithm.cs +++ b/Algorithm.CSharp/ManuallySetMarketHoursAndSymbolPropertiesDatabaseEntriesAlgorithm.cs @@ -16,6 +16,7 @@ using System; using System.Collections.Generic; using QuantConnect.Interfaces; +using QuantConnect.Securities; namespace QuantConnect.Algorithm.CSharp { @@ -54,7 +55,7 @@ public override void Initialize() throw new RegressionTestException("Expected the SPY CFD market hours to be the same as the underlying equity market hours."); } - if (!ReferenceEquals(spyCfd.SymbolProperties, equitySymbolProperties)) + if (!SymbolPropertiesAreEquivalent(spyCfd.SymbolProperties, equitySymbolProperties)) { throw new RegressionTestException("Expected the SPY CFD symbol properties to be the same as the underlying equity symbol properties."); } @@ -74,12 +75,25 @@ public override void Initialize() throw new RegressionTestException("Expected the AUDUSD CFD market hours to be the same as the underlying forex market hours."); } - if (!ReferenceEquals(audUsdCfd.SymbolProperties, audUsdForexSymbolProperties)) + if (!SymbolPropertiesAreEquivalent(audUsdCfd.SymbolProperties, audUsdForexSymbolProperties)) { throw new RegressionTestException("Expected the AUDUSD CFD symbol properties to be the same as the underlying forex symbol properties."); } } + private static bool SymbolPropertiesAreEquivalent(SymbolProperties a, SymbolProperties b) + { + return a.Description == b.Description && + a.QuoteCurrency == b.QuoteCurrency && + a.ContractMultiplier == b.ContractMultiplier && + a.MinimumPriceVariation == b.MinimumPriceVariation && + a.LotSize == b.LotSize && + a.MarketTicker == b.MarketTicker && + a.MinimumOrderSize == b.MinimumOrderSize && + a.PriceMagnifier == b.PriceMagnifier && + a.StrikeMultiplier == b.StrikeMultiplier; + } + /// /// This is used by the regression test system to indicate if the open source Lean repository has the required data to run this algorithm. /// diff --git a/Algorithm.Python/ManuallySetMarketHoursAndSymbolPropertiesDatabaseEntriesAlgorithm.py b/Algorithm.Python/ManuallySetMarketHoursAndSymbolPropertiesDatabaseEntriesAlgorithm.py index 254d49bfb643..780867da0311 100644 --- a/Algorithm.Python/ManuallySetMarketHoursAndSymbolPropertiesDatabaseEntriesAlgorithm.py +++ b/Algorithm.Python/ManuallySetMarketHoursAndSymbolPropertiesDatabaseEntriesAlgorithm.py @@ -51,7 +51,7 @@ def initialize(self): if json.JsonConvert.serialize_object(spy_cfd.exchange.hours) != json.JsonConvert.serialize_object(equity_market_hours_entry.exchange_hours): raise AssertionError("Expected the SPY CFD market hours to be the same as the underlying equity market hours.") - if json.JsonConvert.serialize_object(spy_cfd.symbol_properties) != json.JsonConvert.serialize_object(equity_symbol_properties): + if not self.symbol_properties_are_equivalent(spy_cfd.symbol_properties, equity_symbol_properties): raise AssertionError("Expected the SPY CFD symbol properties to be the same as the underlying equity symbol properties.") # We can also do it for a specific ticker @@ -66,5 +66,16 @@ def initialize(self): if json.JsonConvert.serialize_object(aud_usd_cfd.exchange.hours) != json.JsonConvert.serialize_object(aud_usd_forex_market_hours_entry.exchange_hours): raise AssertionError("Expected the AUDUSD CFD market hours to be the same as the underlying forex market hours.") - if json.JsonConvert.serialize_object(aud_usd_cfd.symbol_properties) != json.JsonConvert.serialize_object(aud_usd_forex_symbol_properties): + if not self.symbol_properties_are_equivalent(aud_usd_cfd.symbol_properties, aud_usd_forex_symbol_properties): raise AssertionError("Expected the AUDUSD CFD symbol properties to be the same as the underlying forex symbol properties.") + + def symbol_properties_are_equivalent(self, a, b): + return (a.description == b.description and + a.quote_currency == b.quote_currency and + a.contract_multiplier == b.contract_multiplier and + a.minimum_price_variation == b.minimum_price_variation and + a.lot_size == b.lot_size and + a.market_ticker == b.market_ticker and + a.minimum_order_size == b.minimum_order_size and + a.price_magnifier == b.price_magnifier and + a.strike_multiplier == b.strike_multiplier) From e0a247215863f5d25ab364c1eb516f8b15793656 Mon Sep 17 00:00:00 2001 From: Josue Nina Date: Mon, 16 Mar 2026 13:19:05 -0500 Subject: [PATCH 6/7] Use ContractSymbolProperties as base class --- Common/Securities/Cfd/Cfd.cs | 10 ++++---- ...perties.cs => ContractSymbolProperties.cs} | 24 +++++++++++++++---- .../Option/OptionSymbolProperties.cs | 22 +---------------- 3 files changed, 25 insertions(+), 31 deletions(-) rename Common/Securities/{Cfd/CfdSymbolProperties.cs => ContractSymbolProperties.cs} (58%) diff --git a/Common/Securities/Cfd/Cfd.cs b/Common/Securities/Cfd/Cfd.cs index 2f74f5e04903..056093bc3d3d 100644 --- a/Common/Securities/Cfd/Cfd.cs +++ b/Common/Securities/Cfd/Cfd.cs @@ -45,7 +45,7 @@ public Cfd(SecurityExchangeHours exchangeHours, IRegisteredSecurityDataTypesProvider registeredTypes) : base(config, quoteCurrency, - new CfdSymbolProperties(symbolProperties), + new ContractSymbolProperties(symbolProperties), new CfdExchange(exchangeHours), new CfdCache(), new SecurityPortfolioModel(), @@ -63,7 +63,7 @@ public Cfd(SecurityExchangeHours exchangeHours, ) { Holdings = new CfdHolding(this, currencyConverter); - _symbolProperties = (CfdSymbolProperties)SymbolProperties; + _symbolProperties = (ContractSymbolProperties)SymbolProperties; } /// @@ -86,7 +86,7 @@ public Cfd(Symbol symbol, SecurityCache securityCache) : base(symbol, quoteCurrency, - new CfdSymbolProperties(symbolProperties), + new ContractSymbolProperties(symbolProperties), new CfdExchange(exchangeHours), securityCache, new SecurityPortfolioModel(), @@ -104,10 +104,10 @@ public Cfd(Symbol symbol, ) { Holdings = new CfdHolding(this, currencyConverter); - _symbolProperties = (CfdSymbolProperties)SymbolProperties; + _symbolProperties = (ContractSymbolProperties)SymbolProperties; } - private readonly CfdSymbolProperties _symbolProperties; + private readonly ContractSymbolProperties _symbolProperties; /// /// Gets or sets the contract multiplier for this CFD security diff --git a/Common/Securities/Cfd/CfdSymbolProperties.cs b/Common/Securities/ContractSymbolProperties.cs similarity index 58% rename from Common/Securities/Cfd/CfdSymbolProperties.cs rename to Common/Securities/ContractSymbolProperties.cs index 10c15b2560dd..c5a3702c1fe4 100644 --- a/Common/Securities/Cfd/CfdSymbolProperties.cs +++ b/Common/Securities/ContractSymbolProperties.cs @@ -13,12 +13,12 @@ * limitations under the License. */ -namespace QuantConnect.Securities.Cfd +namespace QuantConnect.Securities { /// - /// Represents common properties for a specific CFD contract + /// Represents common properties for contract-based securities such as options and CFDs /// - public class CfdSymbolProperties : SymbolProperties + public class ContractSymbolProperties : SymbolProperties { /// /// The contract multiplier for the security. @@ -36,13 +36,27 @@ public class CfdSymbolProperties : SymbolProperties public override decimal ContractMultiplier => _contractMultiplier ?? base.ContractMultiplier; /// - /// Creates an instance of the class from class + /// Creates an instance of the class from a instance /// - public CfdSymbolProperties(SymbolProperties properties) + public ContractSymbolProperties(SymbolProperties properties) : base(properties) { } + /// + /// Creates an instance of the class + /// + public ContractSymbolProperties(string description, string quoteCurrency, decimal contractMultiplier, + decimal minimumPriceVariation, decimal lotSize, string marketTicker, + decimal? minimumOrderSize = null, decimal priceMagnifier = 1, decimal strikeMultiplier = 1) + : base(description, quoteCurrency, contractMultiplier, minimumPriceVariation, lotSize, marketTicker, + minimumOrderSize, priceMagnifier, strikeMultiplier) + { + } + + /// + /// Sets a custom contract multiplier that persists through symbol properties database updates + /// internal void SetContractMultiplier(decimal multiplier) { _contractMultiplier = multiplier; diff --git a/Common/Securities/Option/OptionSymbolProperties.cs b/Common/Securities/Option/OptionSymbolProperties.cs index c01176a9f63a..f6e83f74bce4 100644 --- a/Common/Securities/Option/OptionSymbolProperties.cs +++ b/Common/Securities/Option/OptionSymbolProperties.cs @@ -18,23 +18,8 @@ namespace QuantConnect.Securities.Option /// /// Represents common properties for a specific option contract /// - public class OptionSymbolProperties : SymbolProperties + public class OptionSymbolProperties : ContractSymbolProperties { - /// - /// The contract multiplier for the security. - /// - /// - /// If manually set by a consumer, this value will be used instead of the - /// and also allows to make - /// sure it is not overridden when the symbol properties database gets updated. - /// - private decimal? _contractMultiplier; - - /// - /// The contract multiplier for the security - /// - public override decimal ContractMultiplier => _contractMultiplier ?? base.ContractMultiplier; - /// /// When the holder of an equity option exercises one contract, or when the writer of an equity option is assigned /// an exercise notice on one contract, this unit of trade, usually 100 shares of the underlying security, changes hands. @@ -65,10 +50,5 @@ internal void SetContractUnitOfTrade(int unitOfTrade) { ContractUnitOfTrade = unitOfTrade; } - - internal void SetContractMultiplier(decimal multiplier) - { - _contractMultiplier = multiplier; - } } } From 0878002f15fb87292698aa511af27bd3bf067035 Mon Sep 17 00:00:00 2001 From: Josue Nina Date: Tue, 17 Mar 2026 17:08:35 -0500 Subject: [PATCH 7/7] Minor fix --- Common/Securities/Cfd/Cfd.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Common/Securities/Cfd/Cfd.cs b/Common/Securities/Cfd/Cfd.cs index 056093bc3d3d..4bcadcb2324b 100644 --- a/Common/Securities/Cfd/Cfd.cs +++ b/Common/Securities/Cfd/Cfd.cs @@ -27,6 +27,8 @@ namespace QuantConnect.Securities.Cfd /// public class Cfd : Security { + private readonly ContractSymbolProperties _symbolProperties; + /// /// Constructor for the CFD security /// @@ -107,8 +109,6 @@ public Cfd(Symbol symbol, _symbolProperties = (ContractSymbolProperties)SymbolProperties; } - private readonly ContractSymbolProperties _symbolProperties; - /// /// Gets or sets the contract multiplier for this CFD security ///