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,