Skip to content
Merged
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
11 changes: 7 additions & 4 deletions DataProcessing/QuiverInsiderTradingDataDownloader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -119,18 +119,21 @@ 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();
var acquiredDisposed = insiderTrade.AcquiredDisposedCode.ToCsv();

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)
Expand Down
26 changes: 17 additions & 9 deletions QuiverInsiderTrading.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,13 @@ public class QuiverInsiderTrading : BaseDataCollection
/// </summary>
[JsonProperty(PropertyName = "Date")]
[JsonConverter(typeof(DateTimeJsonConverter), "yyyy-MM-dd")]
public DateTime? Date { get; set; }
public DateTime Date { get; set; }

/// <summary>
/// Time the transaction was filed and became publicly available
/// </summary>
[JsonProperty(PropertyName = "fileDate")]
public DateTime? FileDate { get; set; }
public DateTime FileDate { get; set; }

/// <summary>
/// Type of transaction (see SEC Form 4 codes:
Expand Down Expand Up @@ -83,6 +83,12 @@ public class QuiverInsiderTrading : BaseDataCollection
[JsonProperty(PropertyName = "directOrIndirectOwnership")]
public OwnershipType DirectOrIndirectOwnership { get; set; }

/// <summary>
/// Name of the transactor
/// </summary>
[JsonProperty(PropertyName = "Name")]
public string Name { get; set; }

/// <summary>
/// Corporate title of the transactor
/// </summary>
Expand Down Expand Up @@ -159,18 +165,19 @@ public override BaseData Reader(SubscriptionDataConfig config, string line, Date
Time = uploadedDate.AddDays(-1),
Symbol = config.Symbol,
FileDate = (csv[1].IfNotNullOrEmpty<DateTime?>(s => Parse.DateTimeExact(s, "yyyyMMddHHmmss")) ?? uploadedDate).AddDays(-1),
Date = csv[2].IfNotNullOrEmpty<DateTime?>(s => Parse.DateTimeExact(s, "yyyyMMdd")),
Date = csv[2].IfNotNullOrEmpty<DateTime?>(s => Parse.DateTimeExact(s, "yyyyMMdd")) ?? uploadedDate.AddDays(-1),
TransactionCode = QuiverQuantCsvExtensions.ToTransactionCode(csv[3]),
PricePerShare = csv[4].IfNotNullOrEmpty<decimal?>(s => decimal.Parse(s, NumberStyles.Any, CultureInfo.InvariantCulture)),
Shares = csv[5].IfNotNullOrEmpty<decimal?>(s => decimal.Parse(s, NumberStyles.Any, CultureInfo.InvariantCulture)),
SharesOwnedFollowing = csv[6].IfNotNullOrEmpty<decimal?>(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]),
};
}

Expand All @@ -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}";
}
Expand Down Expand Up @@ -213,6 +220,7 @@ public override BaseData Clone()
SharesOwnedFollowing = SharesOwnedFollowing,
AcquiredDisposedCode = AcquiredDisposedCode,
DirectOrIndirectOwnership = DirectOrIndirectOwnership,
Name = Name,
OfficerTitle = OfficerTitle,
IsDirector = IsDirector,
IsOfficer = IsOfficer,
Expand Down
24 changes: 16 additions & 8 deletions QuiverInsiderTradingUniverse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,12 @@ public class QuiverInsiderTradingUniverse : BaseDataCollection
/// <summary>
/// Transaction date as reported on SEC Form 4
/// </summary>
public DateTime? Date { get; set; }
public DateTime Date { get; set; }

/// <summary>
/// Time the transaction was filed and became publicly available
/// </summary>
public DateTime? FileDate { get; set; }
public DateTime FileDate { get; set; }

/// <summary>
/// Type of transaction (SEC Form 4 code)
Expand Down Expand Up @@ -71,6 +71,11 @@ public class QuiverInsiderTradingUniverse : BaseDataCollection
/// </summary>
public OwnershipType DirectOrIndirectOwnership { get; set; }

/// <summary>
/// Name of the transactor
/// </summary>
public string Name { get; set; }

/// <summary>
/// Corporate title of the transactor
/// </summary>
Expand Down Expand Up @@ -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<DateTime?>(s => Parse.DateTimeExact(s, "yyyyMMddHHmmss")) ?? date).AddDays(-1),
Date = csv[3].IfNotNullOrEmpty<DateTime?>(s => Parse.DateTimeExact(s, "yyyyMMdd")),
Date = csv[3].IfNotNullOrEmpty<DateTime?>(s => Parse.DateTimeExact(s, "yyyyMMdd")) ?? date.AddDays(-1),
TransactionCode = QuiverQuantCsvExtensions.ToTransactionCode(csv[4]),
PricePerShare = price,
Shares = csv[6].IfNotNullOrEmpty<decimal?>(s => decimal.Parse(s, NumberStyles.Any, CultureInfo.InvariantCulture)),
SharesOwnedFollowing = csv[7].IfNotNullOrEmpty<decimal?>(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
};
}
Expand All @@ -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} ") +
Expand All @@ -195,6 +202,7 @@ public override BaseData Clone()
SharesOwnedFollowing = SharesOwnedFollowing,
AcquiredDisposedCode = AcquiredDisposedCode,
DirectOrIndirectOwnership = DirectOrIndirectOwnership,
Name = Name,
OfficerTitle = OfficerTitle,
IsDirector = IsDirector,
IsOfficer = IsOfficer,
Expand Down
28 changes: 24 additions & 4 deletions tests/QuiverInsiderTradingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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);
Expand All @@ -84,29 +85,45 @@ 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);

Assert.AreEqual(new DateTime(2026, 5, 7), result.FileDate);
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()
{
var symbol = new Symbol(SecurityIdentifier.Parse("AAPL R735QTJ8XC9X"), "AAPL");
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);

Assert.AreEqual(TransactionCode.ExerciseOrConversionExempt, result.TransactionCode);
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);
Expand All @@ -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);

Expand All @@ -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);
}

Expand Down Expand Up @@ -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,
Expand Down
Loading