eb1181f5f7
* Adds preliminary universe selection for Future Options
* Fixes scaling issues with Future Options
* Fixes scaling multiplying by 10000x instead of using _scaleFactor
* Fixes scaling for Tick
* Revert changes to Tick since it divides the scaling factor
* Changes stale method name to new method name after rebase
* Fixes selection bugs, adds new methods, and adds unit tests
* Fixes bug where Equity Symbol was created for an underlying
non-equity Symbol, resulting in equity data trying to be loaded
* Adds unit tests covering changes to Tick, QuoteBar, TradeBar and
LeanData
* Adds regression test for AddUniverseOption filter contract selection
for Future Options
* Addresses review - modifies the AddFutureOption signature
* Adds new AddUniverseOptions method overload
* Removes and adds a new unit test
* Misc. modifications to account for new changes
* Fixes bug where futures were loaded using default SID Date
* Refactors and removes unnecessary work
* Fixes regression algorithm, which previously made no trades
* Adds future option data
* Adds the corresponding underlying data, in this case, futures data
to enable usage of future options data
* Replaces data with new data (ES18Z20)
* Improves Future chain filtering and updates regression stats
* Add AddFutureOptionContract API
* Expands regression and unit tests to test in finer detail
* Adds Python regression algorithms for AddFutureOption[Contract] methods
* Adds new unit test for BacktestingOptionChainProvider
* Fixes bug with BacktesingOptionChainProvider where we
attempted to load the Trades option chain first, resulting
in breakage of backwards compatibility and limitation of the
option chain.
* Adds new regression algorithms (Py) to Algorithm.Python project
* Adds FutureOptionMarginBuyingPowerModel
* Modifies code paths used to select margin model
* Adds related unit tests for margin model
* Fixes issue with unit test and MHDB/SPDB lookup for Future Options
* Preliminary regression algorithm testing ITM call/put option buying
* Fixes bug where fee model used did not find non-US market
options fee model. We now use the futures fee model for future
options because IB charges the same commissions per contract
between futures and futures options
* Adds proper regression algorithm for ITM future options expiration
* Pushing broken algorithm for review
* Currently, algorithm does not fill forward, causing
a single future option to not get exercised when it is delisted.
* Adds FutureOptionPutITMExpiryRegressionAlgorithm
* Improves existing regression algorithm for call side
* Fixes bug in existing regression algorithm
* Adds AAPL daily data to advance enumerator for ^^^ fix
* Adds additional future option regression algorithms
* Adds Buy OTM expiration regression algorithms
* Adds Sell ITM/OTM expiration regression algorithms
* Adds missing Python regression algorithms
* Adds remaining Python regression algorithms and fixes issues
* Fixes naming issues and statistics
* Adds short option OTM regression algorithms (Py)
* Add license header and class comments to python algorithms
* Cleans up comments and docstrings
* Create Buy/Sell call intraday regression algo
* Redirects future options symbol properties to futures symbol properties
* Asserts exercise/assignment price and updates stats in regression algos
* Adds new unit test covering changes to SecurityService
* Adds comments and fixes failing test
* Partially fixes future option mis-calculated profit/loss
* Adjusts portfolio model to calculate FOP as a no upfront pay asset class
* Updates regression algorithm statistics
* Begin IB FOP support
* Initial support for FOP IB data streaming, live í¾
* Adds additional functionality to LiveOptionChainProvider
- Allows querying CME API to retrieve option chains for CME products
- Ultimately, it's also the groundwork for the CME
LiveFutureChainProvider
* Edits IDataQueueUniverseProvider interface to provide greater
control to implementors of it
* Misc. bug fixes required to get FOP data streaming through IB
* Adds comments, adds missing rategate call, and cleans up code
* Force exchange for FOP and Futures when no exchange is provided
* Fixes bug with Portfolio modeling across all asset classes
* Adds LiveOptionChainProvider tests for Future Options
* IB brokerage option symbol bug fixes and improvements
* Fixes contract multiplier lookup bug
* Fixes issue where we attempted to subscribe to IB data feed with canonical security
* Adds ES MHDB entry
* Reverts portfolio modeling changes for Futures Options
* Since IB eats into our account's cash balance when
a new FOP contract is purchased, we must model by applying funds
to our cash whenever a new purchase/sell occurs.
If we choose to model FOPs exactly as we do with futures, we
will end up with an invalid TotalPortfolioValue on algorithm
restart. By all means and purposes, FOPs are modeled exactly
the same as equity options with respect to the portfolio.
* Adds comments clarifying portfolio modeling and clarifies
existing portfolio modeling comments with additional context.
* Fixes IB symbol lookup for future options
* Fixes LiveOptionChainProvider looping 5 times per option chain
request, even on success
* Sets OptionChainedUniverseSelectionModel to produce a canonical
future/future option/option Symbol to avoid creating two Symbols
* Adds GLOBEX future option symbol mapping from future -> fop
* Fixes LiveOptionChainProvider loading wrong contract option chains
* Fixes loading of futures options ZIP files when backtesting
* Adds a string -> decimal JSON converter
* Additional fixes/refactoring to the LiveOptionChainProvider
* Adds tests for changes to Symbol and LeanData
* Reverts changes to IB-symbol-map
* Fixes Value for mapped future options tickers
* Fixes Symbol test
* Changes path of future options to future's expiry date
* Extra changes made to remove scaling from writing CSV
* Added method to map from FOP Globex -> FUT Globex
* Fixes MOO and MOC orders for future options
* Note: this order type might not be supported by IB or CME.
* Bug fixes and updates unit tests
* Update regression tests and data format
* Rebase changes
* 1. Multiple bug fixes for LiveOptionChainProvider, reverts IQFeed changes
2. Address review (partial): Code reuse and cleanup
1.
* Modifies check in
`AddFutureOptionShort(Call|Put)ITMExpiryRegressionAlgorithm`
to ensure no buys have negative quantity
* Code reuse changes in IB brokerage
* Bug fix in IB brokerage where we assigned the FOP expiry
as the futures expiry (requires verification)
* Doc changes and adds missing summaries/license banners
* Disposes of HTTP client resources in LiveOptionChainProvider
* Renames classes and adds FutureOption folder in Common/Securities
2.
* We revert back to the quotes API for the option chain,
since the settlement API sometimes had missing strikes.
* Fixes future option expiry being set as future's expiry
in LiveOptionChainProvider
* Fixes bug where wrong option chain was selected because of bad
expiry lookup in the futures expiries returned from CME
* Fixes multiple looping bug in LiveOptionChainProvider
* Adds strike price scaling for LiveOptionChainProvider
* Reverts IQFeed changes and simplifies interface upgrade changes
Some additional challenges we'll have to solve as part of FOPs:
- The `OptionSymbol.IsStandard` method makes the assumption that
weeklies contracts follow the pattern equities follows, which
does not apply to Futures Options
- The Subscription created in:
`OptionChainUniverseSubscriptionEnumeratorFactory`
...adds a Trade config. For illiquid contracts, this
will delay universe selection for the option symbol
until we get a trade. However, if we add a quote config,
the data would instead be loaded based on the first quote
we received from the brokerage.
But since we're currently using a trade config, illiquid
contracts won't start streaming data until it receives a trade.
NOTE: this commit is a WIP to addressing the reviews received in the PR,
but has been committed early for efficiency in the review process
* Fixes regression algorithms and misc. bugs
* Fixes map file lookup for non-equity options
* Adds extra assertion at end of algorithm to ensure no holdings are
left when the algorithm ends.
* Adds FutureOptionSymbol, allowing all contracts through as standard
* Changes SPDB to allow defaulting to underlying future symbol
properties if no entry is found for the given FOP
* Fixes calls to SPDB in SecurityService, IBBrokerage
* Reverts AAPL daily ZIP file to fix majority of regression algorithms
* Adds FOPs symbol properties
* Fixes existing symbol properties for a few futures
* Adds tests for changes to Symbol Properties Database
* Removes string SPDB lookup method
* Updates tests and misc callees of previous method
* Updates all regression tests to use data of already expired contracts
* Adds Futures Options Expiry Functions tests
* Adds required futures data for 2020-01-05
* Address review (partial): Expands test coverage and fixes tests
* Set option chain tests parallelism to fixture only
* Fixes broken test for contract month delta for FuturesOptionsExpiryFunctions
* Changes delisting date logic for Futures Options
* Address review: removes duplicate code, misc code fixes
* Bug fix in MarketHoursDatabase.GetDatabaseSymbolKey() where
we would use the underlying's Symbol for lookup in the MHDB
* Adds missing license banner
* Removes Futures Options entries from MHDB
* Adds new tests
* Adds SecurityType.FutureOption
* Converts any underlying comparisons and uses SecurityType directly
instead for FOP specific behavior
* Extra code modifications to acommodate new SecurityType
* Addresses review: fixes order fee bug on exercise
* Additional bug fixes and adding of SecurityType.FutureOption
* Updates regression algorithms OrderListHash
* Fixes various bugs in IB live implementation
* Fixes bug setting the right contract expiration date for FOP
generated by LiveOptionChainProvider
* Adds new function to FuturesOptionsExpiryFunctions
* Clarifies parameter names better in some functions/methods
* Fixes bugs in IB brokerage for FOPs
* Address review - code cleanup and refactor
* Remove MappingEventProvider, SplitEventProvider, and
DividendEventProvider for Futures Options in
CorporateEventEnumeratorFactory
* Address review: Use MHDB key resolver in SPDB
* Makes regression tests pass and adds comment for expiry issue
* Fixes MHDB lookup on string symbol method
* Adds Futures Options greeks regression algorithm (C# only)
* Adds explanitory comment on MHDB FOP lookup
* Remove python from FutureOptionCallITMGreeksExpiryRegressionAlgorithm
957 lines
40 KiB
C#
957 lines
40 KiB
C#
/*
|
|
* 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.Concurrent;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Numerics;
|
|
using Newtonsoft.Json;
|
|
using ProtoBuf;
|
|
using QuantConnect.Configuration;
|
|
using QuantConnect.Data.UniverseSelection;
|
|
using QuantConnect.Interfaces;
|
|
using QuantConnect.Logging;
|
|
using QuantConnect.Securities.Future;
|
|
using QuantConnect.Util;
|
|
using static QuantConnect.StringExtensions;
|
|
|
|
namespace QuantConnect
|
|
{
|
|
/// <summary>
|
|
/// Defines a unique identifier for securities
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The SecurityIdentifier contains information about a specific security.
|
|
/// This includes the symbol and other data specific to the SecurityType.
|
|
/// The symbol is limited to 12 characters
|
|
/// </remarks>
|
|
[JsonConverter(typeof(SecurityIdentifierJsonConverter))]
|
|
[ProtoContract(SkipConstructor = true)]
|
|
public class SecurityIdentifier : IEquatable<SecurityIdentifier>
|
|
{
|
|
#region Empty, DefaultDate Fields
|
|
|
|
private static readonly ConcurrentDictionary<string, SecurityIdentifier> SecurityIdentifierCache
|
|
= new ConcurrentDictionary<string, SecurityIdentifier>();
|
|
private static readonly string MapFileProviderTypeName = Config.Get("map-file-provider", "LocalDiskMapFileProvider");
|
|
private static readonly char[] InvalidCharacters = {'|', ' '};
|
|
private static readonly Lazy<IMapFileProvider> MapFileProvider = new Lazy<IMapFileProvider>(
|
|
() => Composer.Instance.GetExportedValueByTypeName<IMapFileProvider>(MapFileProviderTypeName)
|
|
);
|
|
|
|
/// <summary>
|
|
/// Gets an instance of <see cref="SecurityIdentifier"/> that is empty, that is, one with no symbol specified
|
|
/// </summary>
|
|
public static readonly SecurityIdentifier Empty = new SecurityIdentifier(string.Empty, 0);
|
|
|
|
/// <summary>
|
|
/// Gets an instance of <see cref="SecurityIdentifier"/> that is explicitly no symbol
|
|
/// </summary>
|
|
public static readonly SecurityIdentifier None = new SecurityIdentifier("NONE", 0);
|
|
|
|
/// <summary>
|
|
/// Gets the date to be used when it does not apply.
|
|
/// </summary>
|
|
public static readonly DateTime DefaultDate = DateTime.FromOADate(0);
|
|
|
|
/// <summary>
|
|
/// Gets the set of invalids symbol characters
|
|
/// </summary>
|
|
public static readonly HashSet<char> InvalidSymbolCharacters = new HashSet<char>(InvalidCharacters);
|
|
|
|
#endregion
|
|
|
|
#region Scales, Widths and Market Maps
|
|
|
|
// these values define the structure of the 'otherData'
|
|
// the constant width fields are used via modulus, so the width is the number of zeros specified,
|
|
// {put/call:1}{oa-date:5}{style:1}{strike:6}{strike-scale:2}{market:3}{security-type:2}
|
|
|
|
private const ulong SecurityTypeWidth = 100;
|
|
private const ulong SecurityTypeOffset = 1;
|
|
|
|
private const ulong MarketWidth = 1000;
|
|
private const ulong MarketOffset = SecurityTypeOffset * SecurityTypeWidth;
|
|
|
|
private const int StrikeDefaultScale = 4;
|
|
private static readonly ulong StrikeDefaultScaleExpanded = Pow(10, StrikeDefaultScale);
|
|
|
|
private const ulong StrikeScaleWidth = 100;
|
|
private const ulong StrikeScaleOffset = MarketOffset * MarketWidth;
|
|
|
|
private const ulong StrikeWidth = 1000000;
|
|
private const ulong StrikeOffset = StrikeScaleOffset * StrikeScaleWidth;
|
|
|
|
private const ulong OptionStyleWidth = 10;
|
|
private const ulong OptionStyleOffset = StrikeOffset * StrikeWidth;
|
|
|
|
private const ulong DaysWidth = 100000;
|
|
private const ulong DaysOffset = OptionStyleOffset * OptionStyleWidth;
|
|
|
|
private const ulong PutCallOffset = DaysOffset * DaysWidth;
|
|
private const ulong PutCallWidth = 10;
|
|
|
|
#endregion
|
|
|
|
#region Member variables
|
|
|
|
[ProtoMember(1)]
|
|
private string _symbol;
|
|
[ProtoMember(2)]
|
|
private ulong _properties;
|
|
[ProtoMember(3)]
|
|
private SecurityIdentifier _underlying;
|
|
private bool _hashCodeSet;
|
|
private int _hashCode;
|
|
private decimal? _strikePrice;
|
|
private OptionStyle? _optionStyle;
|
|
private OptionRight? _optionRight;
|
|
private DateTime? _date;
|
|
private string _stringRep;
|
|
private string _market;
|
|
|
|
#endregion
|
|
|
|
#region Properties
|
|
|
|
/// <summary>
|
|
/// Gets whether or not this <see cref="SecurityIdentifier"/> is a derivative,
|
|
/// that is, it has a valid <see cref="Underlying"/> property
|
|
/// </summary>
|
|
public bool HasUnderlying
|
|
{
|
|
get { return _underlying != null; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the underlying security identifier for this security identifier. When there is
|
|
/// no underlying, this property will return a value of <see cref="Empty"/>.
|
|
/// </summary>
|
|
public SecurityIdentifier Underlying
|
|
{
|
|
get
|
|
{
|
|
if (_underlying == null)
|
|
{
|
|
throw new InvalidOperationException("No underlying specified for this identifier. Check that HasUnderlying is true before accessing the Underlying property.");
|
|
}
|
|
return _underlying;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the date component of this identifier. For equities this
|
|
/// is the first date the security traded. Technically speaking,
|
|
/// in LEAN, this is the first date mentioned in the map_files.
|
|
/// For futures and options this is the expiry date of the contract.
|
|
/// For other asset classes, this property will throw an
|
|
/// exception as the field is not specified.
|
|
/// </summary>
|
|
public DateTime Date
|
|
{
|
|
get
|
|
{
|
|
try
|
|
{
|
|
return _date.Value;
|
|
}
|
|
catch (InvalidOperationException)
|
|
{
|
|
switch (SecurityType)
|
|
{
|
|
case SecurityType.Base:
|
|
case SecurityType.Equity:
|
|
case SecurityType.Option:
|
|
case SecurityType.Future:
|
|
case SecurityType.FutureOption:
|
|
var oadate = ExtractFromProperties(DaysOffset, DaysWidth);
|
|
_date = DateTime.FromOADate(oadate);
|
|
return _date.Value;
|
|
default:
|
|
throw new InvalidOperationException("Date is only defined for SecurityType.Equity, SecurityType.Option, SecurityType.Future, SecurityType.FutureOption, and SecurityType.Base");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the original symbol used to generate this security identifier.
|
|
/// For equities, by convention this is the first ticker symbol for which
|
|
/// the security traded
|
|
/// </summary>
|
|
public string Symbol
|
|
{
|
|
get { return _symbol; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the market component of this security identifier. If located in the
|
|
/// internal mappings, the full string is returned. If the value is unknown,
|
|
/// the integer value is returned as a string.
|
|
/// </summary>
|
|
public string Market
|
|
{
|
|
get
|
|
{
|
|
if (_market == null)
|
|
{
|
|
var marketCode = ExtractFromProperties(MarketOffset, MarketWidth);
|
|
var market = QuantConnect.Market.Decode((int)marketCode);
|
|
// if we couldn't find it, send back the numeric representation
|
|
_market = market ?? marketCode.ToStringInvariant();
|
|
}
|
|
return _market;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the security type component of this security identifier.
|
|
/// </summary>
|
|
[ProtoMember(4)]
|
|
public SecurityType SecurityType { get; }
|
|
|
|
/// <summary>
|
|
/// Gets the option strike price. This only applies to SecurityType.Option
|
|
/// and will thrown anexception if accessed otherwse.
|
|
/// </summary>
|
|
public decimal StrikePrice
|
|
{
|
|
get
|
|
{
|
|
try
|
|
{
|
|
// will throw 'InvalidOperationException' if not set
|
|
return _strikePrice.Value;
|
|
}
|
|
catch (InvalidOperationException)
|
|
{
|
|
if (SecurityType != SecurityType.Option && SecurityType != SecurityType.FutureOption)
|
|
{
|
|
throw new InvalidOperationException("OptionType is only defined for SecurityType.Option and SecurityType.FutureOption");
|
|
}
|
|
|
|
// performance: lets calculate strike price once
|
|
var scale = ExtractFromProperties(StrikeScaleOffset, StrikeScaleWidth);
|
|
var unscaled = ExtractFromProperties(StrikeOffset, StrikeWidth);
|
|
var pow = Math.Pow(10, (int)scale - StrikeDefaultScale);
|
|
// If the 20th bit is set to 1, we have a negative strike price.
|
|
// Let's normalize the strike and explicitly make it negative
|
|
if (((unscaled >> 19) & 1) == 1)
|
|
{
|
|
_strikePrice = -((unscaled ^ 1 << 19) * (decimal)pow);
|
|
}
|
|
else
|
|
{
|
|
_strikePrice = unscaled * (decimal)pow;
|
|
}
|
|
|
|
return _strikePrice.Value;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the option type component of this security identifier. This
|
|
/// only applies to SecurityType.Open and will throw an exception if
|
|
/// accessed otherwise.
|
|
/// </summary>
|
|
public OptionRight OptionRight
|
|
{
|
|
get
|
|
{
|
|
try
|
|
{
|
|
// will throw 'InvalidOperationException' if not set
|
|
return _optionRight.Value;
|
|
}
|
|
catch (InvalidOperationException)
|
|
{
|
|
if (SecurityType != SecurityType.Option && SecurityType != SecurityType.FutureOption)
|
|
{
|
|
throw new InvalidOperationException("OptionRight is only defined for SecurityType.Option and SecurityType.FutureOption");
|
|
}
|
|
_optionRight = (OptionRight)ExtractFromProperties(PutCallOffset, PutCallWidth);
|
|
return _optionRight.Value;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the option style component of this security identifier. This
|
|
/// only applies to SecurityType.Open and will throw an exception if
|
|
/// accessed otherwise.
|
|
/// </summary>
|
|
public OptionStyle OptionStyle
|
|
{
|
|
get
|
|
{
|
|
try
|
|
{
|
|
// will throw 'InvalidOperationException' if not set
|
|
return _optionStyle.Value;
|
|
}
|
|
catch (InvalidOperationException)
|
|
{
|
|
if (SecurityType != SecurityType.Option && SecurityType != SecurityType.FutureOption)
|
|
{
|
|
throw new InvalidOperationException("OptionStyle is only defined for SecurityType.Option and SecurityType.FutureOption");
|
|
}
|
|
|
|
_optionStyle = (OptionStyle)(ExtractFromProperties(OptionStyleOffset, OptionStyleWidth));
|
|
return _optionStyle.Value;
|
|
}
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Constructors
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="SecurityIdentifier"/> class
|
|
/// </summary>
|
|
/// <param name="symbol">The base36 string encoded as a long using alpha [0-9A-Z]</param>
|
|
/// <param name="properties">Other data defining properties of the symbol including market,
|
|
/// security type, listing or expiry date, strike/call/put/style for options, ect...</param>
|
|
public SecurityIdentifier(string symbol, ulong properties)
|
|
{
|
|
if (symbol == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(symbol), "SecurityIdentifier requires a non-null string 'symbol'");
|
|
}
|
|
if (symbol.IndexOfAny(InvalidCharacters) != -1)
|
|
{
|
|
throw new ArgumentException("symbol must not contain the characters '|' or ' '.", nameof(symbol));
|
|
}
|
|
_symbol = symbol;
|
|
_properties = properties;
|
|
_underlying = null;
|
|
_strikePrice = null;
|
|
_optionStyle = null;
|
|
_optionRight = null;
|
|
_date = null;
|
|
SecurityType = (SecurityType)ExtractFromProperties(SecurityTypeOffset, SecurityTypeWidth, properties);
|
|
if (!SecurityType.IsValid())
|
|
{
|
|
throw new ArgumentException($"The provided properties do not match with a valid {nameof(SecurityType)}", "properties");
|
|
}
|
|
_hashCode = unchecked (symbol.GetHashCode() * 397) ^ properties.GetHashCode();
|
|
_hashCodeSet = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="SecurityIdentifier"/> class
|
|
/// </summary>
|
|
/// <param name="symbol">The base36 string encoded as a long using alpha [0-9A-Z]</param>
|
|
/// <param name="properties">Other data defining properties of the symbol including market,
|
|
/// security type, listing or expiry date, strike/call/put/style for options, ect...</param>
|
|
/// <param name="underlying">Specifies a <see cref="SecurityIdentifier"/> that represents the underlying security</param>
|
|
public SecurityIdentifier(string symbol, ulong properties, SecurityIdentifier underlying)
|
|
: this(symbol, properties)
|
|
{
|
|
if (symbol == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(symbol), "SecurityIdentifier requires a non-null string 'symbol'");
|
|
}
|
|
_symbol = symbol;
|
|
_properties = properties;
|
|
// performance: directly call Equals(SecurityIdentifier other), shortcuts Equals(object other)
|
|
if (!underlying.Equals(Empty))
|
|
{
|
|
_underlying = underlying;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region AddMarket, GetMarketCode, and Generate
|
|
|
|
/// <summary>
|
|
/// Generates a new <see cref="SecurityIdentifier"/> for an option
|
|
/// </summary>
|
|
/// <param name="expiry">The date the option expires</param>
|
|
/// <param name="underlying">The underlying security's symbol</param>
|
|
/// <param name="market">The market</param>
|
|
/// <param name="strike">The strike price</param>
|
|
/// <param name="optionRight">The option type, call or put</param>
|
|
/// <param name="optionStyle">The option style, American or European</param>
|
|
/// <returns>A new <see cref="SecurityIdentifier"/> representing the specified option security</returns>
|
|
public static SecurityIdentifier GenerateOption(DateTime expiry,
|
|
SecurityIdentifier underlying,
|
|
string market,
|
|
decimal strike,
|
|
OptionRight optionRight,
|
|
OptionStyle optionStyle)
|
|
{
|
|
return Generate(expiry, underlying.Symbol, QuantConnect.Symbol.GetOptionTypeFromUnderlying(underlying.SecurityType), market, strike, optionRight, optionStyle, underlying);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Generates a new <see cref="SecurityIdentifier"/> for a future
|
|
/// </summary>
|
|
/// <param name="expiry">The date the future expires</param>
|
|
/// <param name="symbol">The security's symbol</param>
|
|
/// <param name="market">The market</param>
|
|
/// <returns>A new <see cref="SecurityIdentifier"/> representing the specified futures security</returns>
|
|
public static SecurityIdentifier GenerateFuture(DateTime expiry,
|
|
string symbol,
|
|
string market)
|
|
{
|
|
return Generate(expiry, symbol, SecurityType.Future, market);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper overload that will search the mapfiles to resolve the first date. This implementation
|
|
/// uses the configured <see cref="IMapFileProvider"/> via the <see cref="Composer.Instance"/>
|
|
/// </summary>
|
|
/// <param name="symbol">The symbol as it is known today</param>
|
|
/// <param name="market">The market</param>
|
|
/// <param name="mapSymbol">Specifies if symbol should be mapped using map file provider</param>
|
|
/// <param name="mapFileProvider">Specifies the IMapFileProvider to use for resolving symbols, specify null to load from Composer</param>
|
|
/// <param name="mappingResolveDate">The date to use to resolve the map file. Default value is <see cref="DateTime.Today"/></param>
|
|
/// <returns>A new <see cref="SecurityIdentifier"/> representing the specified symbol today</returns>
|
|
public static SecurityIdentifier GenerateEquity(string symbol, string market, bool mapSymbol = true, IMapFileProvider mapFileProvider = null, DateTime? mappingResolveDate = null)
|
|
{
|
|
var firstDate = DefaultDate;
|
|
if (mapSymbol)
|
|
{
|
|
var firstTickerDate = GetFirstTickerAndDate(mapFileProvider ?? MapFileProvider.Value, symbol, market, mappingResolveDate: mappingResolveDate);
|
|
firstDate = firstTickerDate.Item2;
|
|
symbol = firstTickerDate.Item1;
|
|
}
|
|
|
|
return GenerateEquity(firstDate, symbol, market);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Generates a new <see cref="SecurityIdentifier"/> for an equity
|
|
/// </summary>
|
|
/// <param name="date">The first date this security traded (in LEAN this is the first date in the map_file</param>
|
|
/// <param name="symbol">The ticker symbol this security traded under on the <paramref name="date"/></param>
|
|
/// <param name="market">The security's market</param>
|
|
/// <returns>A new <see cref="SecurityIdentifier"/> representing the specified equity security</returns>
|
|
public static SecurityIdentifier GenerateEquity(DateTime date, string symbol, string market)
|
|
{
|
|
return Generate(date, symbol, SecurityType.Equity, market);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Generates a new <see cref="SecurityIdentifier"/> for a <see cref="ConstituentsUniverseData"/>.
|
|
/// Note that the symbol ticker is case sensitive here.
|
|
/// </summary>
|
|
/// <param name="symbol">The ticker to use for this constituent identifier</param>
|
|
/// <param name="securityType">The security type of this constituent universe</param>
|
|
/// <param name="market">The security's market</param>
|
|
/// <remarks>This method is special in the sense that it does not force the Symbol to be upper
|
|
/// which is required to determine the source file of the constituent
|
|
/// <see cref="ConstituentsUniverseData.GetSource(Data.SubscriptionDataConfig,DateTime,bool)"/></remarks>
|
|
/// <returns>A new <see cref="SecurityIdentifier"/> representing the specified constituent universe</returns>
|
|
public static SecurityIdentifier GenerateConstituentIdentifier(string symbol, SecurityType securityType, string market)
|
|
{
|
|
return Generate(DefaultDate, symbol, securityType, market, forceSymbolToUpper: false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Generates the <see cref="Symbol"/> property for <see cref="QuantConnect.SecurityType.Base"/> security identifiers
|
|
/// </summary>
|
|
/// <param name="dataType">The base data custom data type if namespacing is required, null otherwise</param>
|
|
/// <param name="symbol">The ticker symbol</param>
|
|
/// <returns>The value used for the security identifier's <see cref="Symbol"/></returns>
|
|
public static string GenerateBaseSymbol(Type dataType, string symbol)
|
|
{
|
|
if (dataType == null)
|
|
{
|
|
return symbol;
|
|
}
|
|
|
|
return $"{symbol.ToUpperInvariant()}.{dataType.Name}";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Generates a new <see cref="SecurityIdentifier"/> for a custom security with the option of providing the first date
|
|
/// </summary>
|
|
/// <param name="dataType">The custom data type</param>
|
|
/// <param name="symbol">The ticker symbol of this security</param>
|
|
/// <param name="market">The security's market</param>
|
|
/// <param name="mapSymbol">Whether or not we should map this symbol</param>
|
|
/// <param name="date">First date that the security traded on</param>
|
|
/// <returns>A new <see cref="SecurityIdentifier"/> representing the specified base security</returns>
|
|
public static SecurityIdentifier GenerateBase(Type dataType, string symbol, string market, bool mapSymbol = false, DateTime? date = null)
|
|
{
|
|
var firstDate = date ?? DefaultDate;
|
|
|
|
if (mapSymbol)
|
|
{
|
|
var firstTickerDate = GetFirstTickerAndDate(MapFileProvider.Value, symbol, market);
|
|
firstDate = firstTickerDate.Item2;
|
|
symbol = firstTickerDate.Item1;
|
|
}
|
|
|
|
return Generate(
|
|
firstDate,
|
|
GenerateBaseSymbol(dataType, symbol),
|
|
SecurityType.Base,
|
|
market,
|
|
forceSymbolToUpper: false
|
|
);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Generates a new <see cref="SecurityIdentifier"/> for a forex pair
|
|
/// </summary>
|
|
/// <param name="symbol">The currency pair in the format similar to: 'EURUSD'</param>
|
|
/// <param name="market">The security's market</param>
|
|
/// <returns>A new <see cref="SecurityIdentifier"/> representing the specified forex pair</returns>
|
|
public static SecurityIdentifier GenerateForex(string symbol, string market)
|
|
{
|
|
return Generate(DefaultDate, symbol, SecurityType.Forex, market);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Generates a new <see cref="SecurityIdentifier"/> for a Crypto pair
|
|
/// </summary>
|
|
/// <param name="symbol">The currency pair in the format similar to: 'EURUSD'</param>
|
|
/// <param name="market">The security's market</param>
|
|
/// <returns>A new <see cref="SecurityIdentifier"/> representing the specified Crypto pair</returns>
|
|
public static SecurityIdentifier GenerateCrypto(string symbol, string market)
|
|
{
|
|
return Generate(DefaultDate, symbol, SecurityType.Crypto, market);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Generates a new <see cref="SecurityIdentifier"/> for a CFD security
|
|
/// </summary>
|
|
/// <param name="symbol">The CFD contract symbol</param>
|
|
/// <param name="market">The security's market</param>
|
|
/// <returns>A new <see cref="SecurityIdentifier"/> representing the specified CFD security</returns>
|
|
public static SecurityIdentifier GenerateCfd(string symbol, string market)
|
|
{
|
|
return Generate(DefaultDate, symbol, SecurityType.Cfd, market);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Generic generate method. This method should be used carefully as some parameters are not required and
|
|
/// some parameters mean different things for different security types
|
|
/// </summary>
|
|
private static SecurityIdentifier Generate(DateTime date,
|
|
string symbol,
|
|
SecurityType securityType,
|
|
string market,
|
|
decimal strike = 0,
|
|
OptionRight optionRight = 0,
|
|
OptionStyle optionStyle = 0,
|
|
SecurityIdentifier underlying = null,
|
|
bool forceSymbolToUpper = true)
|
|
{
|
|
if ((ulong)securityType >= SecurityTypeWidth || securityType < 0)
|
|
{
|
|
throw new ArgumentOutOfRangeException(nameof(securityType), "securityType must be between 0 and 99");
|
|
}
|
|
if ((int)optionRight > 1 || optionRight < 0)
|
|
{
|
|
throw new ArgumentOutOfRangeException(nameof(optionRight), "optionType must be either 0 or 1");
|
|
}
|
|
|
|
// normalize input strings
|
|
market = market.ToLowerInvariant();
|
|
symbol = forceSymbolToUpper ? symbol.LazyToUpper() : symbol;
|
|
|
|
if (securityType == SecurityType.FutureOption)
|
|
{
|
|
// Futures options tickers might not match, so we need
|
|
// to map the provided future Symbol to the actual future option Symbol.
|
|
symbol = FuturesOptionsSymbolMappings.Map(symbol);
|
|
}
|
|
|
|
var marketIdentifier = QuantConnect.Market.Encode(market);
|
|
if (!marketIdentifier.HasValue)
|
|
{
|
|
throw new ArgumentOutOfRangeException(nameof(market), "The specified market wasn't found in the markets lookup. " +
|
|
$"Requested: {market}. You can add markets by calling QuantConnect.Market.AddMarket(string,ushort)"
|
|
);
|
|
}
|
|
|
|
var days = (ulong)date.ToOADate() * DaysOffset;
|
|
var marketCode = (ulong)marketIdentifier * MarketOffset;
|
|
|
|
ulong strikeScale;
|
|
var strk = NormalizeStrike(strike, out strikeScale) * StrikeOffset;
|
|
strikeScale *= StrikeScaleOffset;
|
|
var style = (ulong)optionStyle * OptionStyleOffset;
|
|
var putcall = (ulong)optionRight * PutCallOffset;
|
|
|
|
var otherData = putcall + days + style + strk + strikeScale + marketCode + (ulong)securityType;
|
|
|
|
var result = new SecurityIdentifier(symbol, otherData, underlying ?? Empty);
|
|
|
|
// we already have these so lets set them
|
|
switch (securityType)
|
|
{
|
|
case SecurityType.Base:
|
|
case SecurityType.Equity:
|
|
case SecurityType.Future:
|
|
result._date = date;
|
|
break;
|
|
case SecurityType.Option:
|
|
case SecurityType.FutureOption:
|
|
result._date = date;
|
|
result._strikePrice = strike;
|
|
result._optionRight = optionRight;
|
|
result._optionStyle = optionStyle;
|
|
break;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolves the first ticker/date of the security represented by <paramref name="tickerToday"/>
|
|
/// </summary>
|
|
/// <param name="mapFileProvider">The IMapFileProvider instance used for resolving map files</param>
|
|
/// <param name="tickerToday">The security's ticker as it trades today</param>
|
|
/// <param name="market">The market the security exists in</param>
|
|
/// <param name="mappingResolveDate">The date to use to resolve the map file. Default value is <see cref="DateTime.Today"/></param>
|
|
/// <returns>The security's first ticker/date if mapping data available, otherwise, the provided ticker and DefaultDate are returned</returns>
|
|
private static Tuple<string, DateTime> GetFirstTickerAndDate(IMapFileProvider mapFileProvider, string tickerToday, string market, DateTime? mappingResolveDate = null)
|
|
{
|
|
var resolver = mapFileProvider.Get(market);
|
|
var mapFile = resolver.ResolveMapFile(tickerToday, mappingResolveDate ?? DateTime.Today);
|
|
|
|
// if we have mapping data, use the first ticker/date from there, otherwise use provided ticker and DefaultDate
|
|
return mapFile.Any()
|
|
? Tuple.Create(mapFile.FirstTicker, mapFile.FirstDate)
|
|
: Tuple.Create(tickerToday, DefaultDate);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Converts an upper case alpha numeric string into a long
|
|
/// </summary>
|
|
private static ulong DecodeBase36(string symbol)
|
|
{
|
|
var result = 0ul;
|
|
var baseValue = 1ul;
|
|
for (var i = symbol.Length - 1; i > -1; i--)
|
|
{
|
|
var c = symbol[i];
|
|
|
|
// assumes alpha numeric upper case only strings
|
|
var value = (uint)(c <= 57
|
|
? c - '0'
|
|
: c - 'A' + 10);
|
|
|
|
result += baseValue * value;
|
|
baseValue *= 36;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Converts a long to an uppercase alpha numeric string
|
|
/// </summary>
|
|
private static string EncodeBase36(ulong data)
|
|
{
|
|
var stack = new Stack<char>(15);
|
|
while (data != 0)
|
|
{
|
|
var value = data % 36;
|
|
var c = value < 10
|
|
? (char)(value + '0')
|
|
: (char)(value - 10 + 'A');
|
|
|
|
stack.Push(c);
|
|
data /= 36;
|
|
}
|
|
return new string(stack.ToArray());
|
|
}
|
|
|
|
/// <summary>
|
|
/// The strike is normalized into deci-cents and then a scale factor
|
|
/// is also saved to bring it back to un-normalized
|
|
/// </summary>
|
|
private static ulong NormalizeStrike(decimal strike, out ulong scale)
|
|
{
|
|
var str = strike;
|
|
|
|
if (strike == 0)
|
|
{
|
|
scale = 0;
|
|
return 0;
|
|
}
|
|
|
|
// convert strike to default scaling, this keeps the scale always positive
|
|
strike *= StrikeDefaultScaleExpanded;
|
|
|
|
scale = 0;
|
|
while (strike % 10 == 0)
|
|
{
|
|
strike /= 10;
|
|
scale++;
|
|
}
|
|
|
|
// Since our max precision was previously capped at 999999 and it had 20 bits set,
|
|
// we sacrifice a single bit from the strike price to allow for negative strike prices.
|
|
// 475711 is the maximum value that can be represented when setting the negative bit because
|
|
// any number greater than that will cause an overflow in the strike price width and increase
|
|
// its width to 7 digits.
|
|
// The idea behind this formula is to determine what number the overflow would happen at.
|
|
// We get the max number representable in 19 bits, subtract the width to normalize the value,
|
|
// and then get the difference between the 20 bit mask and the 19 bit normalized value to get
|
|
// the max strike price + 1. Subtract 1 to normalize the value, and we have established an exclusive
|
|
// upper bound.
|
|
const ulong negativeMask = 1 << 19;
|
|
const ulong maxStrikePrice = negativeMask - ((negativeMask ^ (negativeMask - 1)) - StrikeWidth) - 1;
|
|
|
|
if (strike >= maxStrikePrice || strike <= -(long)maxStrikePrice)
|
|
{
|
|
throw new ArgumentException(Invariant($"The specified strike price\'s precision is too high: {str}"));
|
|
}
|
|
|
|
var encodedStrike = (long)strike;
|
|
if (strike < 0)
|
|
{
|
|
// Flip the sign
|
|
encodedStrike = -encodedStrike;
|
|
|
|
// Sets the 20th bit equal to 1
|
|
encodedStrike |= 1 << 19;
|
|
}
|
|
|
|
return (ulong)encodedStrike;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Accurately performs the integer exponentiation
|
|
/// </summary>
|
|
private static ulong Pow(uint x, int pow)
|
|
{
|
|
// don't use Math.Pow(double, double) due to precision issues
|
|
return (ulong)BigInteger.Pow(x, pow);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Parsing routines
|
|
|
|
/// <summary>
|
|
/// Parses the specified string into a <see cref="SecurityIdentifier"/>
|
|
/// The string must be a 40 digit number. The first 20 digits must be parseable
|
|
/// to a 64 bit unsigned integer and contain ancillary data about the security.
|
|
/// The second 20 digits must also be parseable as a 64 bit unsigned integer and
|
|
/// contain the symbol encoded from base36, this provides for 12 alpha numeric case
|
|
/// insensitive characters.
|
|
/// </summary>
|
|
/// <param name="value">The string value to be parsed</param>
|
|
/// <returns>A new <see cref="SecurityIdentifier"/> instance if the <paramref name="value"/> is able to be parsed.</returns>
|
|
/// <exception cref="FormatException">This exception is thrown if the string's length is not exactly 40 characters, or
|
|
/// if the components are unable to be parsed as 64 bit unsigned integers</exception>
|
|
public static SecurityIdentifier Parse(string value)
|
|
{
|
|
Exception exception;
|
|
SecurityIdentifier identifier;
|
|
if (!TryParse(value, out identifier, out exception))
|
|
{
|
|
throw exception;
|
|
}
|
|
|
|
return identifier;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Attempts to parse the specified <see paramref="value"/> as a <see cref="SecurityIdentifier"/>.
|
|
/// </summary>
|
|
/// <param name="value">The string value to be parsed</param>
|
|
/// <param name="identifier">The result of parsing, when this function returns true, <paramref name="identifier"/>
|
|
/// was properly created and reflects the input string, when this function returns false <paramref name="identifier"/>
|
|
/// will equal default(SecurityIdentifier)</param>
|
|
/// <returns>True on success, otherwise false</returns>
|
|
public static bool TryParse(string value, out SecurityIdentifier identifier)
|
|
{
|
|
Exception exception;
|
|
return TryParse(value, out identifier, out exception);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper method impl to be used by parse and tryparse
|
|
/// </summary>
|
|
private static bool TryParse(string value, out SecurityIdentifier identifier, out Exception exception)
|
|
{
|
|
if (!TryParseProperties(value, out exception, out identifier))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private static readonly char[] SplitSpace = {' '};
|
|
|
|
/// <summary>
|
|
/// Parses the string into its component ulong pieces
|
|
/// </summary>
|
|
private static bool TryParseProperties(string value, out Exception exception, out SecurityIdentifier identifier)
|
|
{
|
|
exception = null;
|
|
|
|
if (string.IsNullOrWhiteSpace(value) || value == " 0")
|
|
{
|
|
identifier = Empty;
|
|
return true;
|
|
}
|
|
|
|
// for performance, we first verify if we already have parsed this SecurityIdentifier
|
|
if (SecurityIdentifierCache.TryGetValue(value, out identifier))
|
|
{
|
|
return true;
|
|
}
|
|
// after calling TryGetValue because if it failed it will set identifier to default
|
|
identifier = Empty;
|
|
|
|
try
|
|
{
|
|
var sids = value.Split('|');
|
|
for (var i = sids.Length - 1; i > -1; i--)
|
|
{
|
|
var current = sids[i];
|
|
var parts = current.Split(SplitSpace, StringSplitOptions.RemoveEmptyEntries);
|
|
if (parts.Length != 2)
|
|
{
|
|
exception = new FormatException("The string must be splittable on space into two parts.");
|
|
return false;
|
|
}
|
|
|
|
var symbol = parts[0];
|
|
var otherData = parts[1];
|
|
var props = DecodeBase36(otherData);
|
|
|
|
// toss the previous in as the underlying, if Empty, ignored by ctor
|
|
identifier = new SecurityIdentifier(symbol, props, identifier);
|
|
}
|
|
}
|
|
catch (Exception error)
|
|
{
|
|
exception = error;
|
|
Log.Error($"SecurityIdentifier.TryParseProperties(): Error parsing SecurityIdentifier: '{value}', Exception: {exception}");
|
|
return false;
|
|
}
|
|
|
|
SecurityIdentifierCache.TryAdd(value, identifier);
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Extracts the embedded value from _otherData
|
|
/// </summary>
|
|
private ulong ExtractFromProperties(ulong offset, ulong width)
|
|
{
|
|
return ExtractFromProperties(offset, width, _properties);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Extracts the embedded value from _otherData
|
|
/// </summary>
|
|
/// <remarks>Static so it can be used in <see cref="_lazySecurityType"/> initialization</remarks>
|
|
private static ulong ExtractFromProperties(ulong offset, ulong width, ulong properties)
|
|
{
|
|
return (properties / offset) % width;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Equality members and ToString
|
|
|
|
/// <summary>
|
|
/// Indicates whether the current object is equal to another object of the same type.
|
|
/// </summary>
|
|
/// <returns>
|
|
/// true if the current object is equal to the <paramref name="other"/> parameter; otherwise, false.
|
|
/// </returns>
|
|
/// <param name="other">An object to compare with this object.</param>
|
|
public bool Equals(SecurityIdentifier other)
|
|
{
|
|
return ReferenceEquals(this, other) || _properties == other._properties
|
|
&& _symbol == other._symbol
|
|
&& _underlying == other._underlying;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines whether the specified <see cref="T:System.Object"/> is equal to the current <see cref="T:System.Object"/>.
|
|
/// </summary>
|
|
/// <returns>
|
|
/// true if the specified object is equal to the current object; otherwise, false.
|
|
/// </returns>
|
|
/// <param name="obj">The object to compare with the current object. </param><filterpriority>2</filterpriority>
|
|
public override bool Equals(object obj)
|
|
{
|
|
if (ReferenceEquals(null, obj)) return false;
|
|
if (obj.GetType() != GetType()) return false;
|
|
return Equals((SecurityIdentifier)obj);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Serves as a hash function for a particular type.
|
|
/// </summary>
|
|
/// <returns>
|
|
/// A hash code for the current <see cref="T:System.Object"/>.
|
|
/// </returns>
|
|
/// <filterpriority>2</filterpriority>
|
|
public override int GetHashCode()
|
|
{
|
|
if (!_hashCodeSet)
|
|
{
|
|
_hashCode = unchecked(_symbol.GetHashCode() * 397) ^ _properties.GetHashCode();
|
|
_hashCodeSet = true;
|
|
}
|
|
return _hashCode;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Override equals operator
|
|
/// </summary>
|
|
public static bool operator ==(SecurityIdentifier left, SecurityIdentifier right)
|
|
{
|
|
return Equals(left, right);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Override not equals operator
|
|
/// </summary>
|
|
public static bool operator !=(SecurityIdentifier left, SecurityIdentifier right)
|
|
{
|
|
return !Equals(left, right);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns a string that represents the current object.
|
|
/// </summary>
|
|
/// <returns>
|
|
/// A string that represents the current object.
|
|
/// </returns>
|
|
/// <filterpriority>2</filterpriority>
|
|
public override string ToString()
|
|
{
|
|
if (_stringRep == null)
|
|
{
|
|
var props = EncodeBase36(_properties);
|
|
props = props.Length == 0 ? "0" : props;
|
|
_stringRep = HasUnderlying ? $"{_symbol} {props}|{_underlying}" : $"{_symbol} {props}";
|
|
}
|
|
return _stringRep;
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|