From 06aa6d902dc4af7dc80cb7dbe862d82cf30aac11 Mon Sep 17 00:00:00 2001 From: Alexandre Catarino Date: Fri, 15 May 2026 14:45:14 +0100 Subject: [PATCH] Add insider transactor Name field and tighten Date/FileDate types QuiverQuant's live insiders endpoint returns a Name field for the transactor that wasn't being captured. Adds Name to QuiverInsiderTrading and QuiverInsiderTradingUniverse, stored between ownership and officer title in the CSV row. Reader now also falls back to uploadedDate (minus one day) when the on-disk Date column is empty, and Date/FileDate are non-nullable now that the parser always produces a value. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../QuiverInsiderTradingDataDownloader.cs | 11 +++++--- QuiverInsiderTrading.cs | 26 +++++++++++------ QuiverInsiderTradingUniverse.cs | 24 ++++++++++------ tests/QuiverInsiderTradingTests.cs | 28 ++++++++++++++++--- 4 files changed, 64 insertions(+), 25 deletions(-) diff --git a/DataProcessing/QuiverInsiderTradingDataDownloader.cs b/DataProcessing/QuiverInsiderTradingDataDownloader.cs index 291a282..b1f26ce 100644 --- a/DataProcessing/QuiverInsiderTradingDataDownloader.cs +++ b/DataProcessing/QuiverInsiderTradingDataDownloader.cs @@ -119,10 +119,13 @@ public bool Run(DateTime processDate) var uploadedDate = insiderTrade.Uploaded.Value.Date; // Omit fileDate when its calendar day matches uploaded. Reader falls back to uploadedDate, // preserving the day but dropping intraday precision (acceptable trade-off for storage). - var fileDate = insiderTrade.FileDate?.Date == uploadedDate + var fileDate = insiderTrade.FileDate == default || insiderTrade.FileDate.Date == uploadedDate ? string.Empty - : insiderTrade.FileDate?.ToString("yyyyMMddHHmmss", CultureInfo.InvariantCulture) ?? string.Empty; - var transactionDate = insiderTrade.Date?.ToString("yyyyMMdd", CultureInfo.InvariantCulture) ?? string.Empty; + : insiderTrade.FileDate.ToString("yyyyMMddHHmmss", CultureInfo.InvariantCulture); + var transactionDate = insiderTrade.Date == default + ? string.Empty + : insiderTrade.Date.ToString("yyyyMMdd", CultureInfo.InvariantCulture); + var name = SanitizeCsv(insiderTrade.Name); var officerTitle = SanitizeCsv(insiderTrade.OfficerTitle); var transactionCode = insiderTrade.TransactionCode.ToCsv(); var ownership = insiderTrade.DirectOrIndirectOwnership.ToCsv(); @@ -130,7 +133,7 @@ public bool Run(DateTime processDate) var line = $"{uploadedDate:yyyyMMdd},{fileDate},{transactionDate}," + $"{transactionCode},{insiderTrade.PricePerShare},{insiderTrade.Shares},{insiderTrade.SharesOwnedFollowing}," + - $"{acquiredDisposed},{ownership},{officerTitle}," + + $"{acquiredDisposed},{ownership},{name},{officerTitle}," + $"{insiderTrade.IsDirector.ToCsv()},{insiderTrade.IsOfficer.ToCsv()},{insiderTrade.IsTenPercentOwner.ToCsv()},{insiderTrade.IsOther.ToCsv()}"; foreach (var rawTicker in tickerList) diff --git a/QuiverInsiderTrading.cs b/QuiverInsiderTrading.cs index 6d1ab07..3574ead 100644 --- a/QuiverInsiderTrading.cs +++ b/QuiverInsiderTrading.cs @@ -38,13 +38,13 @@ public class QuiverInsiderTrading : BaseDataCollection /// [JsonProperty(PropertyName = "Date")] [JsonConverter(typeof(DateTimeJsonConverter), "yyyy-MM-dd")] - public DateTime? Date { get; set; } + public DateTime Date { get; set; } /// /// Time the transaction was filed and became publicly available /// [JsonProperty(PropertyName = "fileDate")] - public DateTime? FileDate { get; set; } + public DateTime FileDate { get; set; } /// /// Type of transaction (see SEC Form 4 codes: @@ -83,6 +83,12 @@ public class QuiverInsiderTrading : BaseDataCollection [JsonProperty(PropertyName = "directOrIndirectOwnership")] public OwnershipType DirectOrIndirectOwnership { get; set; } + /// + /// Name of the transactor + /// + [JsonProperty(PropertyName = "Name")] + public string Name { get; set; } + /// /// Corporate title of the transactor /// @@ -159,18 +165,19 @@ public override BaseData Reader(SubscriptionDataConfig config, string line, Date Time = uploadedDate.AddDays(-1), Symbol = config.Symbol, FileDate = (csv[1].IfNotNullOrEmpty(s => Parse.DateTimeExact(s, "yyyyMMddHHmmss")) ?? uploadedDate).AddDays(-1), - Date = csv[2].IfNotNullOrEmpty(s => Parse.DateTimeExact(s, "yyyyMMdd")), + Date = csv[2].IfNotNullOrEmpty(s => Parse.DateTimeExact(s, "yyyyMMdd")) ?? uploadedDate.AddDays(-1), TransactionCode = QuiverQuantCsvExtensions.ToTransactionCode(csv[3]), PricePerShare = csv[4].IfNotNullOrEmpty(s => decimal.Parse(s, NumberStyles.Any, CultureInfo.InvariantCulture)), Shares = csv[5].IfNotNullOrEmpty(s => decimal.Parse(s, NumberStyles.Any, CultureInfo.InvariantCulture)), SharesOwnedFollowing = csv[6].IfNotNullOrEmpty(s => decimal.Parse(s, NumberStyles.Any, CultureInfo.InvariantCulture)), AcquiredDisposedCode = QuiverQuantCsvExtensions.ToAcquiredDisposedCode(csv[7]), DirectOrIndirectOwnership = QuiverQuantCsvExtensions.ToOwnershipType(csv[8]), - OfficerTitle = csv[9], - IsDirector = QuiverQuantCsvExtensions.ToNullableBool(csv[10]), - IsOfficer = QuiverQuantCsvExtensions.ToNullableBool(csv[11]), - IsTenPercentOwner = QuiverQuantCsvExtensions.ToNullableBool(csv[12]), - IsOther = QuiverQuantCsvExtensions.ToNullableBool(csv[13]), + Name = csv[9], + OfficerTitle = csv[10], + IsDirector = QuiverQuantCsvExtensions.ToNullableBool(csv[11]), + IsOfficer = QuiverQuantCsvExtensions.ToNullableBool(csv[12]), + IsTenPercentOwner = QuiverQuantCsvExtensions.ToNullableBool(csv[13]), + IsOther = QuiverQuantCsvExtensions.ToNullableBool(csv[14]), }; } @@ -184,7 +191,7 @@ public override string ToString() // we are the wrapper instance return $"{Symbol} - Data Points {Data.Count}"; } - return $"{Symbol} ({OfficerTitle}) - {TransactionCode}/{AcquiredDisposedCode} - " + + return $"{Symbol} ({Name}, {OfficerTitle}) - {TransactionCode}/{AcquiredDisposedCode} - " + $"{Shares} @ {PricePerShare} - SharesOwnedFollowing: {SharesOwnedFollowing} - " + $"Ownership: {DirectOrIndirectOwnership} - Date: {Date} - Filed: {FileDate}"; } @@ -213,6 +220,7 @@ public override BaseData Clone() SharesOwnedFollowing = SharesOwnedFollowing, AcquiredDisposedCode = AcquiredDisposedCode, DirectOrIndirectOwnership = DirectOrIndirectOwnership, + Name = Name, OfficerTitle = OfficerTitle, IsDirector = IsDirector, IsOfficer = IsOfficer, diff --git a/QuiverInsiderTradingUniverse.cs b/QuiverInsiderTradingUniverse.cs index 57e8228..49c5460 100644 --- a/QuiverInsiderTradingUniverse.cs +++ b/QuiverInsiderTradingUniverse.cs @@ -34,12 +34,12 @@ public class QuiverInsiderTradingUniverse : BaseDataCollection /// /// Transaction date as reported on SEC Form 4 /// - public DateTime? Date { get; set; } + public DateTime Date { get; set; } /// /// Time the transaction was filed and became publicly available /// - public DateTime? FileDate { get; set; } + public DateTime FileDate { get; set; } /// /// Type of transaction (SEC Form 4 code) @@ -71,6 +71,11 @@ public class QuiverInsiderTradingUniverse : BaseDataCollection /// public OwnershipType DirectOrIndirectOwnership { get; set; } + /// + /// Name of the transactor + /// + public string Name { get; set; } + /// /// Corporate title of the transactor /// @@ -143,18 +148,19 @@ public override BaseData Reader(SubscriptionDataConfig config, string line, Date Time = date.AddDays(-1), Symbol = new Symbol(SecurityIdentifier.Parse(csv[0]), csv[1]), FileDate = (csv[2].IfNotNullOrEmpty(s => Parse.DateTimeExact(s, "yyyyMMddHHmmss")) ?? date).AddDays(-1), - Date = csv[3].IfNotNullOrEmpty(s => Parse.DateTimeExact(s, "yyyyMMdd")), + Date = csv[3].IfNotNullOrEmpty(s => Parse.DateTimeExact(s, "yyyyMMdd")) ?? date.AddDays(-1), TransactionCode = QuiverQuantCsvExtensions.ToTransactionCode(csv[4]), PricePerShare = price, Shares = csv[6].IfNotNullOrEmpty(s => decimal.Parse(s, NumberStyles.Any, CultureInfo.InvariantCulture)), SharesOwnedFollowing = csv[7].IfNotNullOrEmpty(s => decimal.Parse(s, NumberStyles.Any, CultureInfo.InvariantCulture)), AcquiredDisposedCode = QuiverQuantCsvExtensions.ToAcquiredDisposedCode(csv[8]), DirectOrIndirectOwnership = QuiverQuantCsvExtensions.ToOwnershipType(csv[9]), - OfficerTitle = csv[10], - IsDirector = QuiverQuantCsvExtensions.ToNullableBool(csv[11]), - IsOfficer = QuiverQuantCsvExtensions.ToNullableBool(csv[12]), - IsTenPercentOwner = QuiverQuantCsvExtensions.ToNullableBool(csv[13]), - IsOther = QuiverQuantCsvExtensions.ToNullableBool(csv[14]), + Name = csv[10], + OfficerTitle = csv[11], + IsDirector = QuiverQuantCsvExtensions.ToNullableBool(csv[12]), + IsOfficer = QuiverQuantCsvExtensions.ToNullableBool(csv[13]), + IsTenPercentOwner = QuiverQuantCsvExtensions.ToNullableBool(csv[14]), + IsOther = QuiverQuantCsvExtensions.ToNullableBool(csv[15]), Value = price ?? 0 }; } @@ -173,6 +179,7 @@ public override string ToString() Invariant($"SharesOwnedFollowing: {SharesOwnedFollowing} ") + Invariant($"AcquiredDisposedCode: {AcquiredDisposedCode} ") + Invariant($"DirectOrIndirectOwnership: {DirectOrIndirectOwnership} ") + + Invariant($"Name: {Name} ") + Invariant($"OfficerTitle: {OfficerTitle} ") + Invariant($"IsDirector: {IsDirector} ") + Invariant($"IsOfficer: {IsOfficer} ") + @@ -195,6 +202,7 @@ public override BaseData Clone() SharesOwnedFollowing = SharesOwnedFollowing, AcquiredDisposedCode = AcquiredDisposedCode, DirectOrIndirectOwnership = DirectOrIndirectOwnership, + Name = Name, OfficerTitle = OfficerTitle, IsDirector = IsDirector, IsOfficer = IsOfficer, diff --git a/tests/QuiverInsiderTradingTests.cs b/tests/QuiverInsiderTradingTests.cs index 104fa14..e162684 100644 --- a/tests/QuiverInsiderTradingTests.cs +++ b/tests/QuiverInsiderTradingTests.cs @@ -55,7 +55,7 @@ public void Reader_ParsesCompactFormat() var symbol = new Symbol(SecurityIdentifier.Parse("AAPL R735QTJ8XC9X"), "AAPL"); var config = CreateConfig(symbol); var factory = new QuiverInsiderTrading(); - var line = "20260508,20260507093000,20260507,P,150.25,100,500,A,D,CEO,T,T,F,"; + var line = "20260508,20260507093000,20260507,P,150.25,100,500,A,D,John Smith,CEO,T,T,F,"; var result = (QuiverInsiderTrading)factory.Reader(config, line, new DateTime(2026, 5, 8), false); @@ -70,6 +70,7 @@ public void Reader_ParsesCompactFormat() Assert.AreEqual(500m, result.SharesOwnedFollowing); Assert.AreEqual(AcquiredDisposedCode.Acquired, result.AcquiredDisposedCode); Assert.AreEqual(OwnershipType.Direct, result.DirectOrIndirectOwnership); + Assert.AreEqual("John Smith", result.Name); Assert.AreEqual("CEO", result.OfficerTitle); Assert.AreEqual(true, result.IsDirector); Assert.AreEqual(true, result.IsOfficer); @@ -84,7 +85,7 @@ public void Reader_EmptyFileDateFallsBackToUploadedMinusOne() var config = CreateConfig(symbol); var factory = new QuiverInsiderTrading(); // csv[1] (fileDate) is empty — Reader uses uploadedDate.AddDays(-1) - var line = "20260508,,20260507,S,275,1534,13366,D,D,CFO,,T,,"; + var line = "20260508,,20260507,S,275,1534,13366,D,D,Jane Doe,CFO,,T,,"; var result = (QuiverInsiderTrading)factory.Reader(config, line, new DateTime(2026, 5, 8), false); @@ -92,6 +93,21 @@ public void Reader_EmptyFileDateFallsBackToUploadedMinusOne() Assert.AreEqual(new DateTime(2026, 5, 7), result.Time); } + [Test] + public void Reader_EmptyDateFallsBackToUploadedMinusOne() + { + var symbol = new Symbol(SecurityIdentifier.Parse("AAPL R735QTJ8XC9X"), "AAPL"); + var config = CreateConfig(symbol); + var factory = new QuiverInsiderTrading(); + // csv[2] (Date) is empty — Reader falls back to uploadedDate.AddDays(-1) + var line = "20260508,,,,,1717,40879,,,Jane Doe,,,,,"; + + var result = (QuiverInsiderTrading)factory.Reader(config, line, new DateTime(2026, 5, 8), false); + + Assert.AreEqual(new DateTime(2026, 5, 7), result.Date); + Assert.AreEqual(new DateTime(2026, 5, 7), result.FileDate); + } + [Test] public void Reader_EmptyOptionalFieldsAreNull() { @@ -99,7 +115,7 @@ public void Reader_EmptyOptionalFieldsAreNull() var config = CreateConfig(symbol); var factory = new QuiverInsiderTrading(); // All optional numerics/booleans empty - var line = "20260508,,20260507,M,,1717,40879,A,D,,,,,"; + var line = "20260508,,20260507,M,,1717,40879,A,D,,,,,,"; var result = (QuiverInsiderTrading)factory.Reader(config, line, new DateTime(2026, 5, 8), false); @@ -107,6 +123,7 @@ public void Reader_EmptyOptionalFieldsAreNull() Assert.IsNull(result.PricePerShare); Assert.AreEqual(1717m, result.Shares); Assert.AreEqual(40879m, result.SharesOwnedFollowing); + Assert.AreEqual(string.Empty, result.Name); Assert.AreEqual(string.Empty, result.OfficerTitle); Assert.IsNull(result.IsDirector); Assert.IsNull(result.IsOfficer); @@ -119,7 +136,7 @@ public void UniverseReader_ParsesCompactFormat() { var factory = new QuiverInsiderTradingUniverse(); // csv[0]=sid, csv[1]=ticker, csv[2]=fileDate(empty -> fallback), csv[3]=Date, csv[4]=TransactionCode, ... - var line = "AAPL R735QTJ8XC9X,AAPL,,20260507,P,150.25,100,500,A,D,CEO,T,T,F,"; + var line = "AAPL R735QTJ8XC9X,AAPL,,20260507,P,150.25,100,500,A,D,John Smith,CEO,T,T,F,"; var result = (QuiverInsiderTradingUniverse)factory.Reader(null, line, new DateTime(2026, 5, 8), false); @@ -131,6 +148,8 @@ public void UniverseReader_ParsesCompactFormat() Assert.AreEqual(150.25m, result.PricePerShare); Assert.AreEqual(AcquiredDisposedCode.Acquired, result.AcquiredDisposedCode); Assert.AreEqual(OwnershipType.Direct, result.DirectOrIndirectOwnership); + Assert.AreEqual("John Smith", result.Name); + Assert.AreEqual("CEO", result.OfficerTitle); Assert.AreEqual(150.25m, result.Value); } @@ -186,6 +205,7 @@ private BaseData CreateNewInstance() SharesOwnedFollowing = 0.0m, AcquiredDisposedCode = AcquiredDisposedCode.Acquired, DirectOrIndirectOwnership = OwnershipType.Direct, + Name = "John Smith", OfficerTitle = "CEO", IsDirector = false, IsOfficer = true,