/*
* 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
{
///
/// Defines a unique identifier for securities
///
///
/// 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
///
[JsonConverter(typeof(SecurityIdentifierJsonConverter))]
[ProtoContract(SkipConstructor = true)]
public class SecurityIdentifier : IEquatable
{
#region Empty, DefaultDate Fields
private static readonly ConcurrentDictionary SecurityIdentifierCache
= new ConcurrentDictionary();
private static readonly string MapFileProviderTypeName = Config.Get("map-file-provider", "LocalDiskMapFileProvider");
private static readonly char[] InvalidCharacters = {'|', ' '};
private static readonly Lazy MapFileProvider = new Lazy(
() => Composer.Instance.GetExportedValueByTypeName(MapFileProviderTypeName)
);
///
/// Gets an instance of that is empty, that is, one with no symbol specified
///
public static readonly SecurityIdentifier Empty = new SecurityIdentifier(string.Empty, 0);
///
/// Gets an instance of that is explicitly no symbol
///
public static readonly SecurityIdentifier None = new SecurityIdentifier("NONE", 0);
///
/// Gets the date to be used when it does not apply.
///
public static readonly DateTime DefaultDate = DateTime.FromOADate(0);
///
/// Gets the set of invalids symbol characters
///
public static readonly HashSet InvalidSymbolCharacters = new HashSet(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
///
/// Gets whether or not this is a derivative,
/// that is, it has a valid property
///
public bool HasUnderlying
{
get { return _underlying != null; }
}
///
/// Gets the underlying security identifier for this security identifier. When there is
/// no underlying, this property will return a value of .
///
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;
}
}
///
/// 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.
///
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");
}
}
}
}
///
/// 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
///
public string Symbol
{
get { return _symbol; }
}
///
/// 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.
///
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;
}
}
///
/// Gets the security type component of this security identifier.
///
[ProtoMember(4)]
public SecurityType SecurityType { get; }
///
/// Gets the option strike price. This only applies to SecurityType.Option
/// and will thrown anexception if accessed otherwse.
///
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;
}
}
}
///
/// Gets the option type component of this security identifier. This
/// only applies to SecurityType.Open and will throw an exception if
/// accessed otherwise.
///
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;
}
}
}
///
/// Gets the option style component of this security identifier. This
/// only applies to SecurityType.Open and will throw an exception if
/// accessed otherwise.
///
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
///
/// Initializes a new instance of the class
///
/// The base36 string encoded as a long using alpha [0-9A-Z]
/// Other data defining properties of the symbol including market,
/// security type, listing or expiry date, strike/call/put/style for options, ect...
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;
}
///
/// Initializes a new instance of the class
///
/// The base36 string encoded as a long using alpha [0-9A-Z]
/// Other data defining properties of the symbol including market,
/// security type, listing or expiry date, strike/call/put/style for options, ect...
/// Specifies a that represents the underlying security
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
///
/// Generates a new for an option
///
/// The date the option expires
/// The underlying security's symbol
/// The market
/// The strike price
/// The option type, call or put
/// The option style, American or European
/// A new representing the specified option security
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);
}
///
/// Generates a new for a future
///
/// The date the future expires
/// The security's symbol
/// The market
/// A new representing the specified futures security
public static SecurityIdentifier GenerateFuture(DateTime expiry,
string symbol,
string market)
{
return Generate(expiry, symbol, SecurityType.Future, market);
}
///
/// Helper overload that will search the mapfiles to resolve the first date. This implementation
/// uses the configured via the
///
/// The symbol as it is known today
/// The market
/// Specifies if symbol should be mapped using map file provider
/// Specifies the IMapFileProvider to use for resolving symbols, specify null to load from Composer
/// The date to use to resolve the map file. Default value is
/// A new representing the specified symbol today
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);
}
///
/// Generates a new for an equity
///
/// The first date this security traded (in LEAN this is the first date in the map_file
/// The ticker symbol this security traded under on the
/// The security's market
/// A new representing the specified equity security
public static SecurityIdentifier GenerateEquity(DateTime date, string symbol, string market)
{
return Generate(date, symbol, SecurityType.Equity, market);
}
///
/// Generates a new for a .
/// Note that the symbol ticker is case sensitive here.
///
/// The ticker to use for this constituent identifier
/// The security type of this constituent universe
/// The security's market
/// 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
///
/// A new representing the specified constituent universe
public static SecurityIdentifier GenerateConstituentIdentifier(string symbol, SecurityType securityType, string market)
{
return Generate(DefaultDate, symbol, securityType, market, forceSymbolToUpper: false);
}
///
/// Generates the property for security identifiers
///
/// The base data custom data type if namespacing is required, null otherwise
/// The ticker symbol
/// The value used for the security identifier's
public static string GenerateBaseSymbol(Type dataType, string symbol)
{
if (dataType == null)
{
return symbol;
}
return $"{symbol.ToUpperInvariant()}.{dataType.Name}";
}
///
/// Generates a new for a custom security with the option of providing the first date
///
/// The custom data type
/// The ticker symbol of this security
/// The security's market
/// Whether or not we should map this symbol
/// First date that the security traded on
/// A new representing the specified base security
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
);
}
///
/// Generates a new for a forex pair
///
/// The currency pair in the format similar to: 'EURUSD'
/// The security's market
/// A new representing the specified forex pair
public static SecurityIdentifier GenerateForex(string symbol, string market)
{
return Generate(DefaultDate, symbol, SecurityType.Forex, market);
}
///
/// Generates a new for a Crypto pair
///
/// The currency pair in the format similar to: 'EURUSD'
/// The security's market
/// A new representing the specified Crypto pair
public static SecurityIdentifier GenerateCrypto(string symbol, string market)
{
return Generate(DefaultDate, symbol, SecurityType.Crypto, market);
}
///
/// Generates a new for a CFD security
///
/// The CFD contract symbol
/// The security's market
/// A new representing the specified CFD security
public static SecurityIdentifier GenerateCfd(string symbol, string market)
{
return Generate(DefaultDate, symbol, SecurityType.Cfd, market);
}
///
/// 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
///
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;
}
///
/// Resolves the first ticker/date of the security represented by
///
/// The IMapFileProvider instance used for resolving map files
/// The security's ticker as it trades today
/// The market the security exists in
/// The date to use to resolve the map file. Default value is
/// The security's first ticker/date if mapping data available, otherwise, the provided ticker and DefaultDate are returned
private static Tuple 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);
}
///
/// Converts an upper case alpha numeric string into a long
///
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;
}
///
/// Converts a long to an uppercase alpha numeric string
///
private static string EncodeBase36(ulong data)
{
var stack = new Stack(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());
}
///
/// The strike is normalized into deci-cents and then a scale factor
/// is also saved to bring it back to un-normalized
///
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;
}
///
/// Accurately performs the integer exponentiation
///
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
///
/// Parses the specified string into a
/// 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.
///
/// The string value to be parsed
/// A new instance if the is able to be parsed.
/// 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
public static SecurityIdentifier Parse(string value)
{
Exception exception;
SecurityIdentifier identifier;
if (!TryParse(value, out identifier, out exception))
{
throw exception;
}
return identifier;
}
///
/// Attempts to parse the specified as a .
///
/// The string value to be parsed
/// The result of parsing, when this function returns true,
/// was properly created and reflects the input string, when this function returns false
/// will equal default(SecurityIdentifier)
/// True on success, otherwise false
public static bool TryParse(string value, out SecurityIdentifier identifier)
{
Exception exception;
return TryParse(value, out identifier, out exception);
}
///
/// Helper method impl to be used by parse and tryparse
///
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 = {' '};
///
/// Parses the string into its component ulong pieces
///
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;
}
///
/// Extracts the embedded value from _otherData
///
private ulong ExtractFromProperties(ulong offset, ulong width)
{
return ExtractFromProperties(offset, width, _properties);
}
///
/// Extracts the embedded value from _otherData
///
/// Static so it can be used in initialization
private static ulong ExtractFromProperties(ulong offset, ulong width, ulong properties)
{
return (properties / offset) % width;
}
#endregion
#region Equality members and ToString
///
/// Indicates whether the current object is equal to another object of the same type.
///
///
/// true if the current object is equal to the parameter; otherwise, false.
///
/// An object to compare with this object.
public bool Equals(SecurityIdentifier other)
{
return ReferenceEquals(this, other) || _properties == other._properties
&& _symbol == other._symbol
&& _underlying == other._underlying;
}
///
/// Determines whether the specified is equal to the current .
///
///
/// true if the specified object is equal to the current object; otherwise, false.
///
/// The object to compare with the current object. 2
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (obj.GetType() != GetType()) return false;
return Equals((SecurityIdentifier)obj);
}
///
/// Serves as a hash function for a particular type.
///
///
/// A hash code for the current .
///
/// 2
public override int GetHashCode()
{
if (!_hashCodeSet)
{
_hashCode = unchecked(_symbol.GetHashCode() * 397) ^ _properties.GetHashCode();
_hashCodeSet = true;
}
return _hashCode;
}
///
/// Override equals operator
///
public static bool operator ==(SecurityIdentifier left, SecurityIdentifier right)
{
return Equals(left, right);
}
///
/// Override not equals operator
///
public static bool operator !=(SecurityIdentifier left, SecurityIdentifier right)
{
return !Equals(left, right);
}
///
/// Returns a string that represents the current object.
///
///
/// A string that represents the current object.
///
/// 2
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
}
}