Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 92 additions & 2 deletions Common/Securities/SecurityDefinitionSymbolResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,15 +66,21 @@ private SecurityDefinitionSymbolResolver(IDataProvider dataProvider = null, stri
/// to get the ticker of the symbol on this date.
/// </param>
/// <returns>The Lean Symbol corresponding to the CUSIP number on the trading date provided</returns>
/// <remarks>Accepts both the full 9-character CUSIP and its 8-character base, without the check digit.
/// A 9-character CUSIP whose check digit does not match is not resolved.</remarks>
public Symbol CUSIP(string cusip, DateTime tradingDate)
{
if (string.IsNullOrWhiteSpace(cusip))
{
return null;
}

var hasCusipBase = TryGetCUSIPBase(cusip, out var cusipBase);
return SecurityDefinitionToSymbol(
GetSecurityDefinitions().FirstOrDefault(x => x.CUSIP != null && x.CUSIP.Equals(cusip, StringComparison.InvariantCultureIgnoreCase)),
GetSecurityDefinitions().FirstOrDefault(x => x.CUSIP != null &&
(x.CUSIP.Equals(cusip, StringComparison.InvariantCultureIgnoreCase) ||
hasCusipBase && TryGetCUSIPBase(x.CUSIP, out var definitionCusipBase) &&
definitionCusipBase.Equals(cusipBase, StringComparison.InvariantCultureIgnoreCase))),
tradingDate);
}

Expand All @@ -83,9 +89,15 @@ public Symbol CUSIP(string cusip, DateTime tradingDate)
/// </summary>
/// <param name="symbol">The Lean <see cref="Symbol"/></param>
/// <returns>The Committee on Uniform Securities Identification Procedures (CUSIP) number corresponding to the given Lean <see cref="Symbol"/></returns>
/// <remarks>The CUSIP is returned with its 9th character, the check digit, even when the security definition only provides the 8-character base</remarks>
public string CUSIP(Symbol symbol)
{
return SymbolToSecurityDefinition(symbol)?.CUSIP;
var cusip = SymbolToSecurityDefinition(symbol)?.CUSIP;
if (cusip?.Length == 8 && TryGetCUSIPCheckDigit(cusip, out var checkDigit))
{
return cusip.ToUpperInvariant() + checkDigit;
}
return cusip;
}

/// <summary>
Expand Down Expand Up @@ -273,6 +285,84 @@ private SecurityDefinition SymbolToSecurityDefinition(Symbol symbol)
return GetSecurityDefinitions().FirstOrDefault(x => x.SecurityIdentifier.Equals(symbol.ID));
}

/// <summary>
/// Gets the 8-character base of a CUSIP, the part identifying the issuer and the issue.
/// A 9-character CUSIP is only accepted when its check digit is valid.
/// </summary>
private static bool TryGetCUSIPBase(string cusip, out string cusipBase)
{
cusipBase = null;
if (cusip == null || !TryGetCUSIPCheckDigit(cusip, out var checkDigit))
{
return false;
}

if (cusip.Length == 8)
{
cusipBase = cusip;
}
else if (cusip.Length == 9 && cusip[8] == checkDigit)
{
cusipBase = cusip.Substring(0, 8);
}

return cusipBase != null;
}

/// <summary>
/// Calculates the CUSIP check digit (the 9th character) from the first 8 characters of a CUSIP,
/// using the "modulus 10 double add double" algorithm. See https://en.wikipedia.org/wiki/CUSIP
/// </summary>
private static bool TryGetCUSIPCheckDigit(string cusip, out char checkDigit)
{
checkDigit = default;
if (cusip.Length < 8)
{
return false;
}

var sum = 0;
for (var i = 0; i < 8; i++)
{
var c = char.ToUpperInvariant(cusip[i]);
int value;
if (c >= '0' && c <= '9')
{
value = c - '0';
}
else if (c >= 'A' && c <= 'Z')
{
value = c - 'A' + 10;
}
else if (c == '*')
{
value = 36;
}
else if (c == '@')
{
value = 37;
}
else if (c == '#')
{
value = 38;
}
else
{
return false;
}

// every second character is doubled
if (i % 2 == 1)
{
value *= 2;
}
sum += value / 10 + value % 10;
}

checkDigit = (char)('0' + (10 - sum % 10) % 10);
return true;
}

/// <summary>
/// Get's the security definitions using a lazy initialization
/// </summary>
Expand Down
44 changes: 37 additions & 7 deletions Tests/Common/Securities/SecurityDefinitionSymbolResolverTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ public class SecurityDefinitionSymbolResolverTests
{"QQQ", "QQQ RIWIV7K5Z9LX"},
{"QQQQ", "QQQ RIWIV7K5Z9LX"}
};
private static readonly Dictionary<string, string> _cusipWithCheckDigit = new Dictionary<string, string>
{
{"03783310", "037833100"},
{"38259P70", "38259P706"},
{"73935A10", "73935A104"}
};

[OneTimeSetUp]
public void SetUp()
Expand All @@ -48,7 +54,7 @@ public void SetUp()

var securityDatabaseLines = string.Join("\n",
"AAPL R735QTJ8XC9X,03783310,BBG000B9XRY4,2046251,US0378331005,320193",
"GOOG T1AZ164W5VTX,38259P50,BBG000BHSKN9,B020QX2,US38259P5089,",
"GOOG T1AZ164W5VTX,38259P508,BBG000BHSKN9,B020QX2,US38259P5089,",
"GOOCV VP83T1ZUHROL,38259P70,BBG002W96FT9,BKM4JZ7,US38259P7069,",
"QQQ RIWIV7K5Z9LX,73935A10,BBG000BSWKH7,BDQYP67,US46090E1038,");
File.WriteAllText(securityDatabaseFilePath,securityDatabaseLines);
Expand Down Expand Up @@ -109,10 +115,20 @@ public void ResolveCIK()
[TestCase("73935A10", 1999, 3, 11, "QQQ", Market.USA)]
[TestCase("73935A10", 1999, 3, 10, "QQQ", Market.USA)]
[TestCase("73935A10", 1998, 5, 21, "QQQ", Market.USA)]
[TestCase("037833100", 2021, 9, 9, "AAPL", Market.USA)]
[TestCase("38259P706", 2021, 9, 9, "GOOG", Market.USA)]
[TestCase("38259p706", 2014, 4, 2, "GOOCV", Market.USA)]
[TestCase("73935a104", 2011, 3, 22, "QQQQ", Market.USA)]
[TestCase("73935A104", 1998, 5, 21, "QQQ", Market.USA)]
[TestCase("", 2021, 9, 9, null, Market.USA)]
[TestCase("ABCDEF99", 2021, 9, 9, null, Market.USA)]
[TestCase(null, 2021, 9, 9, null, Market.USA)]
[TestCase("1", 2021, 9, 9, null, Market.USA)]
[TestCase("0378331", 2021, 9, 9, null, Market.USA)]
[TestCase("037833101", 2021, 9, 9, null, Market.USA)]
[TestCase("38259P705", 2021, 9, 9, null, Market.USA)]
[TestCase("0378331000", 2021, 9, 9, null, Market.USA)]
[TestCase("037833!0", 2021, 9, 9, null, Market.USA)]
public void ResolvesCUSIP(string cusip, int year, int month, int day, string expectedTicker, string expectedMarket)
{
var tradingDate = new DateTime(year, month, day);
Expand All @@ -124,7 +140,21 @@ public void ResolvesCUSIP(string cusip, int year, int month, int day, string exp

AssertSymbol(symbol, expectedTicker, expectedSid, expectedMarket);

AssertSymbolIdentifier(symbol, _instance.CUSIP(symbol), cusip);
// the resolved CUSIP always carries the check digit, even when looked up by the 8-character base
var expectedCusip = cusip != null && _cusipWithCheckDigit.TryGetValue(cusip.ToUpperInvariant(), out var fullCusip) ? fullCusip : cusip;
AssertSymbolIdentifier(symbol, _instance.CUSIP(symbol), expectedCusip);
}

[TestCase("38259P50")]
[TestCase("38259P508")]
[TestCase("38259p508")]
public void ResolvesCUSIPStoredWithCheckDigit(string cusip)
{
var symbol = _instance.CUSIP(cusip, new DateTime(2021, 9, 9));

Assert.IsNotNull(symbol);
Assert.AreEqual("GOOG T1AZ164W5VTX", symbol.ID.ToString());
Assert.AreEqual("38259P508", _instance.CUSIP(symbol));
}

[TestCaseSource(nameof(SymbolToCUSIPTestCases))]
Expand Down Expand Up @@ -292,11 +322,11 @@ private static void AssertSymbolIdentifier(Symbol symbol, string reconvertedIden

private static TestCaseData[] SymbolToCUSIPTestCases => new[]
{
new TestCaseData(Symbol.Create("AAPL", SecurityType.Equity, Market.USA), "03783310"),
new TestCaseData(Symbol.Create("GOOG", SecurityType.Equity, Market.USA), "38259P70"),
new TestCaseData(Symbol.Create("GOOCV", SecurityType.Equity, Market.USA), "38259P70"),
new TestCaseData(Symbol.Create("QQQ", SecurityType.Equity, Market.USA), "73935A10"),
new TestCaseData(Symbol.Create("QQQQ", SecurityType.Equity, Market.USA), "73935A10"),
new TestCaseData(Symbol.Create("AAPL", SecurityType.Equity, Market.USA), "037833100"),
new TestCaseData(Symbol.Create("GOOG", SecurityType.Equity, Market.USA), "38259P706"),
new TestCaseData(Symbol.Create("GOOCV", SecurityType.Equity, Market.USA), "38259P706"),
new TestCaseData(Symbol.Create("QQQ", SecurityType.Equity, Market.USA), "73935A104"),
new TestCaseData(Symbol.Create("QQQQ", SecurityType.Equity, Market.USA), "73935A104"),
new TestCaseData(Symbol.Create("ABCD", SecurityType.Equity, Market.USA), null),
new TestCaseData(null, null)
};
Expand Down