From 19b23473ecf53342f44d0758a4d33449390602e6 Mon Sep 17 00:00:00 2001 From: Alexandre Catarino Date: Tue, 12 May 2026 02:55:56 +0100 Subject: [PATCH 1/2] Refactor CNBC and InsiderTrading data formats for storage efficiency - Extract TransactionCode, OwnershipType, AcquiredDisposedCode enums under the QuantConnect.DataSource.QuiverQuant namespace, each annotated with [EnumMember] for the SEC single-letter code so Newtonsoft serializes them directly without a custom converter. - Persist enum values, booleans, and OrderDirection in CSV as single letters / T-F / underlying int (helpers in QuiverQuantCsvExtensions), shrinking on-disk size for both datasets. - InsiderTrading now reads every field returned by live/insiders: Date, fileDate, TransactionCode, PricePerShare, Shares, SharesOwnedFollowing, AcquiredDisposedCode, DirectOrIndirectOwnership, OfficerTitle, IsDirector/IsOfficer/IsTenPercentOwner/IsOther. - Reader semantics: Time = uploadedDate.AddDays(-1) so EndTime equals the upload day. fileDate / adviceDate empty-shortcut falls back to that upload date (CNBC and InsiderTrading aligned). - InsiderTrading downloader mirrors the CNBC pattern: Run accumulates per-ticker in memory, Flush writes per-ticker files with per-ticker exception handling, ProcessUniverse rebuilds universe files from the per-ticker corpus by upload date. - Reject invalid tickers (e.g. "N/A") in the InsiderTrading Run loop to avoid Windows path-separator failures. - Program.cs lifts processing-date / processing-date-lookback so a single invocation can backfill recent days; CNBC and InsiderTrading share the same iteration loop. Dataset is now selectable via args[0]. - Add tests covering the Reader and universe Reader for the compact format plus every CSV helper mapping (33 helper cases + 8 Reader cases). Co-Authored-By: Claude Opus 4.7 (1M context) --- AcquiredDisposedCode.cs | 46 ++++ DataProcessing/Program.cs | 67 +++-- DataProcessing/QuiverCNBCDataDownloader.cs | 168 +++++++++--- .../QuiverInsiderTradingDataDownloader.cs | 255 +++++++++++------- OwnershipType.cs | 46 ++++ QuiverCNBC.cs | 20 +- QuiverCNBCsUniverse.cs | 21 +- QuiverInsiderTrading.cs | 120 +++++++-- QuiverInsiderTradingUniverse.cs | 120 +++++++-- QuiverQuantCsvExtensions.cs | 128 +++++++++ TransactionCode.cs | 150 +++++++++++ tests/QuiverCNBCTests.cs | 77 ++++++ tests/QuiverInsiderTradingTests.cs | 107 +++++++- tests/QuiverQuantCsvExtensionsTests.cs | 120 +++++++++ 14 files changed, 1220 insertions(+), 225 deletions(-) create mode 100644 AcquiredDisposedCode.cs create mode 100644 OwnershipType.cs create mode 100644 QuiverQuantCsvExtensions.cs create mode 100644 TransactionCode.cs create mode 100644 tests/QuiverQuantCsvExtensionsTests.cs diff --git a/AcquiredDisposedCode.cs b/AcquiredDisposedCode.cs new file mode 100644 index 0000000..7046cf2 --- /dev/null +++ b/AcquiredDisposedCode.cs @@ -0,0 +1,46 @@ +/* + * 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.Runtime.Serialization; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace QuantConnect.DataSource.QuiverQuant +{ + /// + /// SEC Form 4 indicator of whether the transaction was an acquisition or a disposal + /// + [JsonConverter(typeof(StringEnumConverter))] + public enum AcquiredDisposedCode + { + /// + /// Default value used when no acquired/disposed flag is provided or the value is unrecognized + /// + [EnumMember(Value = "")] + Unknown, + + /// + /// A - Share acquisition + /// + [EnumMember(Value = "A")] + Acquired, + + /// + /// D - Share disposal + /// + [EnumMember(Value = "D")] + Disposed, + } +} diff --git a/DataProcessing/Program.cs b/DataProcessing/Program.cs index 52fb61e..0c3d0b7 100644 --- a/DataProcessing/Program.cs +++ b/DataProcessing/Program.cs @@ -32,34 +32,46 @@ public class Program /// Exit code. 0 equals successful, and any other value indicates the downloader/converter failed. public static void Main(string[] args) { - var dataset = args[0]; + var dataset = args.Length > 0 ? args[0].ToLowerInvariant() : "cnbc"; var destinationDirectory = Path.Combine( Config.Get("temp-output-directory", "/temp-output-directory"), "alternative"); var processedDataDirectory = Path.Combine( Config.Get("processed-data-directory", Globals.DataFolder), "alternative"); - var processingDateValue = Config.Get("processing-date", Environment.GetEnvironmentVariable("QC_DATAFLEET_DEPLOYMENT_DATE")); + var processingDateValue = Config.Get("processing-date", Environment.GetEnvironmentVariable("QC_DATAFLEET_DEPLOYMENT_DATE")) + ?? DateTime.UtcNow.AddDays(-1).ToString("yyyyMMdd"); + var processingDate = Parse.DateTimeExact(processingDateValue, "yyyyMMdd"); + var processingDateLookback = Config.GetInt("processing-date-lookback", 0); + var processingStartDate = processingDate.AddDays(-processingDateLookback); switch (dataset.ToLowerInvariant()) { case "cnbc": { - var processingDate = Parse.DateTimeExact(processingDateValue, "yyyyMMdd"); RunDownloader( - QuiverCNBCDataDownloader.VendorName, + QuiverDataDownloader.VendorName, QuiverCNBCDataDownloader.VendorDataName, () => new QuiverCNBCDataDownloader(destinationDirectory, processedDataDirectory), - instance => instance.Run(processingDate)); + instance => + { + for (var date = processingStartDate; date <= processingDate; date = date.AddDays(1)) + { + if (!instance.Run(date)) + { + Log.Error($"QuantConnect.DataProcessing.Program.Main(): Failed to download/process " + + $"{QuiverDataDownloader.VendorName} {QuiverCNBCDataDownloader.VendorDataName} data for date: {date:yyyy-MM-dd}"); + } + } + instance.Flush(); + instance.ProcessUniverse(); + return true; + }); break; } case "governmentcontract": { - var processingDate = string.IsNullOrWhiteSpace(processingDateValue) - ? DateTime.UtcNow.AddDays(-1) - : Parse.DateTimeExact(processingDateValue, "yyyyMMdd"); - var datasetStartDate = new DateTime(2022, 4, 21); if (processingDate < datasetStartDate) { @@ -68,7 +80,7 @@ public static void Main(string[] args) } RunDownloader( - QuiverGovernmentContractDownloader.VendorName, + QuiverDataDownloader.VendorName, QuiverGovernmentContractDownloader.VendorDataName, () => new QuiverGovernmentContractDownloader(), instance => @@ -77,7 +89,7 @@ public static void Main(string[] args) if (!success) { Log.Error($"QuantConnect.DataProcessing.Program.Main(): Failed to download/process " + - $"{QuiverGovernmentContractDownloader.VendorName} {QuiverGovernmentContractDownloader.VendorDataName} data for date: {processingDate:yyyy-MM-dd}"); + $"{QuiverDataDownloader.VendorName} {QuiverGovernmentContractDownloader.VendorDataName} data for date: {processingDate:yyyy-MM-dd}"); } instance.ProcessUniverse(); return true; @@ -87,9 +99,8 @@ public static void Main(string[] args) case "lobbying": { - var processingDate = Parse.DateTimeExact(processingDateValue, "yyyyMMdd"); RunDownloader( - QuiverLobbyingDataDownloader.VendorName, + QuiverDataDownloader.VendorName, QuiverLobbyingDataDownloader.VendorDataName, () => new QuiverLobbyingDataDownloader(destinationDirectory, processedDataDirectory), instance => instance.Run(processingDate)); @@ -100,7 +111,7 @@ public static void Main(string[] args) { var congressDestination = Path.Combine(destinationDirectory, "quiver"); RunDownloader( - QuiverCongressDataDownloader.VendorName, + QuiverDataDownloader.VendorName, QuiverCongressDataDownloader.VendorDataName, () => new QuiverCongressDataDownloader(congressDestination), instance => instance.Run()); @@ -111,7 +122,7 @@ public static void Main(string[] args) { var tempOutput = Config.Get("temp-output-directory", "/temp-output-directory"); RunDownloader( - QuiverWallStreetBetsDataDownloader.VendorName, + QuiverDataDownloader.VendorName, QuiverWallStreetBetsDataDownloader.VendorDataName, () => new QuiverWallStreetBetsDataDownloader(tempOutput), instance => instance.Run()); @@ -120,13 +131,24 @@ public static void Main(string[] args) case "insidertrading": { - var processingStartDate = GetDateConfig("processing-start-date"); - var processingEndDate = GetDateConfig("processing-end-date"); RunDownloader( - QuiverInsiderTradingDataDownloader.VendorName, + QuiverDataDownloader.VendorName, QuiverInsiderTradingDataDownloader.VendorDataName, () => new QuiverInsiderTradingDataDownloader(destinationDirectory, processedDataDirectory), - instance => instance.Run(processingStartDate, processingEndDate)); + instance => + { + for (var date = processingStartDate; date <= processingDate; date = date.AddDays(1)) + { + if (!instance.Run(date)) + { + Log.Error($"QuantConnect.DataProcessing.Program.Main(): Failed to download/process " + + $"{QuiverDataDownloader.VendorName} {QuiverInsiderTradingDataDownloader.VendorDataName} data for date: {date:yyyy-MM-dd}"); + } + } + instance.Flush(); + instance.ProcessUniverse(); + return true; + }); break; } @@ -169,12 +191,5 @@ private static void RunDownloader(string vendorName, string vendorDataName, F instance.DisposeSafely(); } } - - private static DateTime GetDateConfig(string configKey) - { - var value = Config.Get(configKey, Environment.GetEnvironmentVariable("QC_DATAFLEET_DEPLOYMENT_DATE")) - ?? DateTime.Today.ToString("yyyyMMdd"); - return Parse.DateTimeExact(value, "yyyyMMdd"); - } } } \ No newline at end of file diff --git a/DataProcessing/QuiverCNBCDataDownloader.cs b/DataProcessing/QuiverCNBCDataDownloader.cs index 6a58e04..e40032b 100644 --- a/DataProcessing/QuiverCNBCDataDownloader.cs +++ b/DataProcessing/QuiverCNBCDataDownloader.cs @@ -23,6 +23,7 @@ using Newtonsoft.Json; using QuantConnect.Data.Auxiliary; using QuantConnect.DataSource; +using QuantConnect.DataSource.QuiverQuant; using QuantConnect.Lean.Engine.DataFeeds; using QuantConnect.Logging; using QuantConnect.Util; @@ -39,6 +40,7 @@ public class QuiverCNBCDataDownloader : QuiverDataDownloader private readonly string _destinationFolder; private readonly string _universeFolder; private readonly string _processedDataDirectory; + private readonly Dictionary> _cnbcByTicker = []; /// /// Creates a new instance of @@ -84,16 +86,10 @@ public bool Run(DateTime processDate) var cnbcByDate = JsonConvert.DeserializeObject>(quiverCnbcData, _jsonSerializerSettings); - var cnbcByTicker = new Dictionary>(); - var universeCsvContents = new List(); - - var mapFileProvider = new LocalZipMapFileProvider(); - mapFileProvider.Initialize(new DefaultDataProvider()); - foreach (var cnbc in cnbcByDate) { var ticker = cnbc.Ticker; - if (ticker == null) + if (ticker == null) { Log.Error($"QuiverCNBCDataDownloader.Run(): Null value for Ticker on {processDate:yyyyMMdd}"); continue; @@ -111,29 +107,113 @@ public bool Run(DateTime processDate) continue; } - if (!cnbcByTicker.TryGetValue(ticker, out var _)) + var note = SanitizeCsv(cnbc.Notes); + var traders = SanitizeCsv(cnbc.Traders); + var curRow = $"{cnbc.Direction.ToCsv()},{traders},{note}"; + var uploadDate = cnbc.UploadDate?.Date ?? processDate; + // csv[0] is always the uploadDate. csv[1] (adviceDate) is omitted when it equals uploadDate; + // Reader falls back to uploadedDate in that case. + var adviceDateCol = uploadDate == processDate + ? string.Empty + : $"{processDate:yyyyMMdd}"; + var line = $"{uploadDate:yyyyMMdd},{adviceDateCol},{curRow}"; + + if (!_cnbcByTicker.TryGetValue(ticker, out var lines)) { - cnbcByTicker.Add(ticker, new List()); + _cnbcByTicker[ticker] = lines = []; } + lines.Add(line); + } + } + catch (Exception e) + { + Log.Error(e); + return false; + } - var note = cnbc.Notes != null ? cnbc.Notes.Replace(Environment.NewLine, string.Empty).Trim() : null; - var curRow = $"{note},{cnbc.Direction},{cnbc.Traders.Trim()}"; - cnbcByTicker[ticker].Add($"{processDate:yyyyMMdd},{curRow}"); + Log.Trace($"QuiverCNBCDataDownloader.Run(): Finished in {stopwatch.Elapsed.ToStringInvariant(null)}"); + return true; + } - var sid = SecurityIdentifier.GenerateEquity(ticker, Market.USA, true, mapFileProvider, processDate); - universeCsvContents.Add($"{sid},{ticker},{curRow}"); + /// + /// Writes every accumulated per-ticker batch to disk, merging with any pre-existing file. + /// + /// True on success + public bool Flush() + { + try + { + foreach (var kvp in _cnbcByTicker) + { + SaveContentToFile(_destinationFolder, kvp.Key, kvp.Value); } + } + catch (Exception e) + { + Log.Error(e); + return false; + } + return true; + } + + /// + /// Regenerates the universe files keyed by upload date by reading every per-ticker file. + /// + /// True if the universe files were regenerated successfully + public bool ProcessUniverse() + { + if (!_canCreateUniverseFiles) + { + Log.Trace($"QuiverCNBCDataDownloader.ProcessUniverse(): Map files not available, skipping universe generation"); + return false; + } + + var stopwatch = Stopwatch.StartNew(); + Log.Trace($"QuiverCNBCDataDownloader.ProcessUniverse(): Start regenerating universe files by upload date"); + + try + { + var mapFileProvider = new LocalZipMapFileProvider(); + mapFileProvider.Initialize(new DefaultDataProvider()); + + Dictionary> dataByUploadDate = []; - if (!_canCreateUniverseFiles) + void processFile(string filePath) { - return false; + var ticker = Path.GetFileNameWithoutExtension(filePath).ToUpperInvariant(); + foreach (var line in File.ReadAllLines(filePath)) + { + var firstComma = line.IndexOf(','); + if (firstComma <= 0) continue; + + var uploadDateStr = line[..firstComma]; + if (!DateTime.TryParseExact(uploadDateStr, "yyyyMMdd", CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal, out var uploadDate)) + { + continue; + } + var rest = line[(firstComma + 1)..]; + + if (!dataByUploadDate.TryGetValue(uploadDate, out var data)) + { + dataByUploadDate[uploadDate] = data = []; + } + + var sid = SecurityIdentifier.GenerateEquity(ticker, Market.USA, true, mapFileProvider, uploadDate); + data.Add($"{sid},{ticker},{rest}"); + } } - else if (universeCsvContents.Any()) + + if (Directory.Exists(_processedDataDirectory)) { - SaveContentToFile(_universeFolder, $"{processDate:yyyyMMdd}", universeCsvContents); + Directory.EnumerateFiles(_processedDataDirectory, "*.csv").DoForEach(processFile); } + Directory.EnumerateFiles(_destinationFolder, "*.csv").DoForEach(processFile); - cnbcByTicker.DoForEach(kvp => SaveContentToFile(_destinationFolder, kvp.Key, kvp.Value)); + dataByUploadDate.DoForEach(kvp => + { + var filePath = Path.Combine(_universeFolder, $"{kvp.Key:yyyyMMdd}.csv"); + File.WriteAllLines(filePath, kvp.Value.OrderBy(x => x)); + }); } catch (Exception e) { @@ -141,46 +221,45 @@ public bool Run(DateTime processDate) return false; } - Log.Trace($"QuiverCNBCDataDownloader.Run(): Finished in {stopwatch.Elapsed.ToStringInvariant(null)}"); + Log.Trace($"QuiverCNBCDataDownloader.ProcessUniverse(): Finished in {stopwatch.Elapsed.ToStringInvariant(null)}"); return true; } /// - /// Saves contents to disk, deleting existing zip files + /// Saves per-ticker contents to disk, merging with any pre-existing file /// /// Final destination of the data /// file name /// Contents to write - private void SaveContentToFile(string destinationFolder, string name, IEnumerable contents) + private static string SanitizeCsv(string value) { - name = name.ToLowerInvariant(); - var finalPath = Path.Combine(destinationFolder, $"{name}.csv"); - string filePath; - - if (destinationFolder.Contains("universe")) + if (string.IsNullOrEmpty(value)) { - filePath = Path.Combine(_processedDataDirectory, "universe", $"{name}.csv"); - } - else - { - filePath = Path.Combine(_processedDataDirectory, $"{name}.csv"); + return string.Empty; } + return value.Replace(",", string.Empty).Replace("\r", string.Empty).Replace("\n", string.Empty).Trim(); + } - var finalFileExists = File.Exists(filePath); + private void SaveContentToFile(string destinationFolder, string name, IEnumerable contents) + { + name = name.ToLowerInvariant(); + var finalPath = Path.Combine(destinationFolder, $"{name}.csv"); + var filePath = Path.Combine(_processedDataDirectory, $"{name}.csv"); - var lines = new HashSet(contents); - if (finalFileExists) + HashSet lines = [.. contents]; + foreach (var path in new[] { filePath, finalPath }) { - foreach (var line in File.ReadAllLines(filePath)) + if (File.Exists(path)) { - lines.Add(line); + foreach (var line in File.ReadAllLines(path)) + { + lines.Add(line); + } } } - var finalLines = destinationFolder.Contains("universe") ? - lines.OrderBy(x => x.Split(',').First()).ToList() : - lines - .OrderBy(x => DateTime.ParseExact(x.Split(',').First(), "yyyyMMdd", CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal)) + var finalLines = lines + .OrderBy(x => DateTime.ParseExact(x.Split(',')[0], "yyyyMMdd", CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal)) .ToList(); File.WriteAllLines(finalPath, finalLines); @@ -200,6 +279,13 @@ private class RawCNBC: QuiverCNBC /// [JsonProperty(PropertyName = "Ticker")] public string Ticker { get; set; } + + /// + /// The date this data was uploaded to QuiverQuant's database + /// + [JsonProperty(PropertyName = "upload_time")] + [JsonConverter(typeof(DateTimeJsonConverter), "yyyy-MM-dd")] + public DateTime? UploadDate { get; set; } } } diff --git a/DataProcessing/QuiverInsiderTradingDataDownloader.cs b/DataProcessing/QuiverInsiderTradingDataDownloader.cs index 95d6dea..f091c33 100644 --- a/DataProcessing/QuiverInsiderTradingDataDownloader.cs +++ b/DataProcessing/QuiverInsiderTradingDataDownloader.cs @@ -19,9 +19,11 @@ using System.Globalization; using System.IO; using System.Linq; +using System.Text.RegularExpressions; using Newtonsoft.Json; using QuantConnect.Data.Auxiliary; using QuantConnect.DataSource; +using QuantConnect.DataSource.QuiverQuant; using QuantConnect.Lean.Engine.DataFeeds; using QuantConnect.Logging; using QuantConnect.Util; @@ -38,6 +40,8 @@ public class QuiverInsiderTradingDataDownloader : QuiverDataDownloader private readonly string _destinationFolder; private readonly string _universeFolder; private readonly string _processedDataDirectory; + private readonly Dictionary> _insiderTradingByTicker = []; + private static readonly List _defunctDelimiters = new() { '-', @@ -69,33 +73,14 @@ public QuiverInsiderTradingDataDownloader() } /// - /// Runs the instance of the object with a given date. - /// - /// First date of data to be fetched and processed - /// Last date of data to be fetched and processed - /// True if process last downloads successfully - public bool Run(DateTime processingStartDate, DateTime processingEndDate) - { - var success = false; - - for (var processDate= processingStartDate; processDate<= processingEndDate; processDate = processDate.AddDays(1)) - { - success = Run(processDate); - } - - return success; - } - - /// - /// Runs the instance of the object with a given date. + /// Fetches a single day of insider trading data and accumulates it per-ticker in memory. /// - /// The date of data to be fetched and processed - /// True if process all downloads successfully + /// The date of data to be fetched + /// True if the day was fetched and parsed successfully public bool Run(DateTime processDate) { - var symbolsProcessed = new List(); var stopwatch = Stopwatch.StartNew(); - Log.Trace($"QuiverInsiderTradingDataDownloader.Run(): Start downloading/processing QuiverQuant Insider Trading data"); + Log.Trace($"QuiverInsiderTradingDataDownloader.Run(): Start downloading QuiverQuant Insider Trading data for {processDate:yyyy-MM-dd}"); var today = DateTime.UtcNow.Date; try @@ -105,74 +90,167 @@ public bool Run(DateTime processDate) Log.Trace($"Encountered data from invalid date: {processDate:yyyy-MM-dd} - Skipping"); return false; } - + var quiverInsiderTradingData = HttpRequester($"live/insiders?date={processDate:yyyyMMdd}").SynchronouslyAwaitTaskResult(); if (string.IsNullOrWhiteSpace(quiverInsiderTradingData)) { - // We've already logged inside HttpRequester return false; } var insiderTradingByDate = JsonConvert.DeserializeObject>(quiverInsiderTradingData, _jsonSerializerSettings); - var insiderTradingByTicker = new Dictionary>(); - var universeCsvContents = new List(); - - var mapFileProvider = new LocalZipMapFileProvider(); - mapFileProvider.Initialize(new DefaultDataProvider()); - foreach (var insiderTrade in insiderTradingByDate) { var quiverTicker = insiderTrade.Ticker; if (quiverTicker == null) continue; + if (insiderTrade.Uploaded == null) + { + Log.Trace($"QuiverInsiderTradingDataDownloader.Run(): Skipping row with null Uploaded for ticker {quiverTicker} on {processDate:yyyyMMdd}"); + continue; + } + if (!TryNormalizeDefunctTicker(quiverTicker, out var tickerList)) { - Log.Error( - $"QuiverInsiderTradingDataDownloader.Run(): Defunct ticker {quiverTicker} is unable to be parsed. Continuing..."); + Log.Error($"QuiverInsiderTradingDataDownloader.Run(): Defunct ticker {quiverTicker} is unable to be parsed. Continuing..."); continue; } - foreach (var ticker in tickerList) + 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 + ? string.Empty + : insiderTrade.FileDate?.ToString("yyyyMMddHHmmss", CultureInfo.InvariantCulture) ?? string.Empty; + var transactionDate = insiderTrade.Date?.ToString("yyyyMMdd", CultureInfo.InvariantCulture) ?? string.Empty; + 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}," + + $"{insiderTrade.IsDirector.ToCsv()},{insiderTrade.IsOfficer.ToCsv()},{insiderTrade.IsTenPercentOwner.ToCsv()},{insiderTrade.IsOther.ToCsv()}"; + + foreach (var rawTicker in tickerList) { - var sid = default(SecurityIdentifier); - try + var ticker = Regex.Replace(rawTicker, @"[^A-Z0-9.]", string.Empty).Trim('.'); + if (!Regex.IsMatch(ticker, @"^[A-Z0-9][A-Z0-9.]*[A-Z0-9]$") && !Regex.IsMatch(ticker, @"^[A-Z0-9]$")) { - sid = SecurityIdentifier.GenerateEquity(ticker, Market.USA, true, mapFileProvider, processDate); + Log.Trace($"QuiverInsiderTradingDataDownloader.Run(): Skipping invalid ticker '{rawTicker}' on {processDate:yyyyMMdd}"); + continue; } - catch (Exception) + if (!_insiderTradingByTicker.TryGetValue(ticker, out var lines)) + { + _insiderTradingByTicker[ticker] = lines = []; + } + lines.Add(line); + } + } + } + catch (Exception e) + { + Log.Error(e); + return false; + } + + Log.Trace($"QuiverInsiderTradingDataDownloader.Run(): Finished in {stopwatch.Elapsed.ToStringInvariant(null)}"); + return true; + } + + /// + /// Writes every accumulated per-ticker batch to disk, merging with any pre-existing file. + /// + /// True on success + public bool Flush() + { + var failed = 0; + foreach (var kvp in _insiderTradingByTicker) + { + try + { + SaveContentToFile(kvp.Key, kvp.Value); + } + catch (Exception e) + { + failed++; + Log.Error(e, $"QuiverInsiderTradingDataDownloader.Flush(): Failed to write data for ticker '{kvp.Key}'"); + } + } + return failed == 0; + } + + /// + /// Regenerates the universe files keyed by upload date by reading every per-ticker file. + /// + /// True if the universe files were regenerated successfully + public bool ProcessUniverse() + { + if (!_canCreateUniverseFiles) + { + Log.Trace("QuiverInsiderTradingDataDownloader.ProcessUniverse(): Map files not available, skipping universe generation"); + return false; + } + + var stopwatch = Stopwatch.StartNew(); + Log.Trace("QuiverInsiderTradingDataDownloader.ProcessUniverse(): Start regenerating universe files by upload date"); + + try + { + var mapFileProvider = new LocalZipMapFileProvider(); + mapFileProvider.Initialize(new DefaultDataProvider()); + + Dictionary> dataByUploadDate = []; + + void processFile(string filePath) + { + var ticker = Path.GetFileNameWithoutExtension(filePath).ToUpperInvariant(); + foreach (var line in File.ReadAllLines(filePath)) + { + var firstComma = line.IndexOf(','); + if (firstComma <= 0) continue; + + var uploadDateStr = line[..firstComma]; + if (!DateTime.TryParseExact(uploadDateStr, "yyyyMMdd", CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal, out var uploadDate)) { - Log.Error($"QuiverInsiderTradingDataDownloader.Run(): Invalid ticker {ticker}. Continuing..."); continue; } + var rest = line[(firstComma + 1)..]; - symbolsProcessed.Add(ticker); - - if (sid.Date == SecurityIdentifier.DefaultDate || sid.ToString().Contains(" 2T")) continue; + if (!dataByUploadDate.TryGetValue(uploadDate, out var data)) + { + dataByUploadDate[uploadDate] = data = []; + } - if (!insiderTradingByTicker.TryGetValue(ticker, out var _)) + SecurityIdentifier sid; + try { - insiderTradingByTicker.Add(ticker, new List()); + sid = SecurityIdentifier.GenerateEquity(ticker, Market.USA, true, mapFileProvider, uploadDate); + } + catch (Exception) + { + Log.Error($"QuiverInsiderTradingDataDownloader.ProcessUniverse(): Invalid ticker {ticker} on {uploadDate:yyyyMMdd}. Skipping line."); + continue; } - var curRow = $"{insiderTrade.Name.Replace(",", string.Empty).Trim().ToLower()},{insiderTrade.Shares},{insiderTrade.PricePerShare},{insiderTrade.SharesOwnedFollowing}"; - insiderTradingByTicker[ticker].Add($"{processDate:yyyyMMdd},{curRow}"); + if (sid.Date == SecurityIdentifier.DefaultDate || sid.ToString().Contains(" 2T")) continue; - universeCsvContents.Add($"{sid},{ticker},{curRow}"); + data.Add($"{sid},{ticker},{rest}"); } } - if (!_canCreateUniverseFiles) - { - return false; - } - if (universeCsvContents.Any()) + if (Directory.Exists(_processedDataDirectory)) { - SaveContentToFile(_universeFolder, $"{processDate:yyyyMMdd}", universeCsvContents); + Directory.EnumerateFiles(_processedDataDirectory, "*.csv").DoForEach(processFile); } + Directory.EnumerateFiles(_destinationFolder, "*.csv").DoForEach(processFile); - insiderTradingByTicker.DoForEach(kvp => SaveContentToFile(_destinationFolder, kvp.Key, kvp.Value)); - Log.Trace($"QuiverInsiderTradingDataDownloader.Run(): Processed tickers for {processDate:yyyyMMdd} - {String.Join(", ", symbolsProcessed)}"); + dataByUploadDate.DoForEach(kvp => + { + var filePath = Path.Combine(_universeFolder, $"{kvp.Key:yyyyMMdd}.csv"); + File.WriteAllLines(filePath, kvp.Value.OrderBy(x => x)); + }); } catch (Exception e) { @@ -180,49 +258,49 @@ public bool Run(DateTime processDate) return false; } - Log.Trace($"QuiverInsiderTradingDataDownloader.Run(): Finished in {stopwatch.Elapsed.ToStringInvariant(null)}"); + Log.Trace($"QuiverInsiderTradingDataDownloader.ProcessUniverse(): Finished in {stopwatch.Elapsed.ToStringInvariant(null)}"); return true; } /// - /// Saves contents to disk, deleting existing zip files + /// Saves per-ticker contents to disk, merging with any pre-existing file. /// - /// Final destination of the data - /// File name + /// File name (ticker) /// Contents to write - private void SaveContentToFile(string destinationFolder, string name, IEnumerable contents) + private void SaveContentToFile(string name, IEnumerable contents) { - var finalPath = Path.Combine(destinationFolder, $"{name.ToLowerInvariant()}.csv"); - string filePath; + name = name.ToLowerInvariant(); + var finalPath = Path.Combine(_destinationFolder, $"{name}.csv"); + var existingPath = Path.Combine(_processedDataDirectory, $"{name}.csv"); - if (destinationFolder.Contains("universe")) - { - filePath = Path.Combine(_processedDataDirectory, "universe", $"{name}.csv"); - } - else + HashSet lines = [.. contents]; + foreach (var path in new[] { existingPath, finalPath }) { - filePath = Path.Combine(_processedDataDirectory, $"{name.ToLowerInvariant()}.csv"); - } - - var finalFileExists = File.Exists(filePath); - - var lines = new HashSet(contents); - if (finalFileExists) - { - foreach (var line in File.ReadAllLines(filePath)) + if (File.Exists(path)) { - lines.Add(line); + foreach (var line in File.ReadAllLines(path)) + { + lines.Add(line); + } } } - var finalLines = destinationFolder.Contains("universe") - ? lines.OrderBy(x => x) - : lines.OrderBy(x => DateTime.ParseExact(x.Split(',').First(), "yyyyMMdd", - CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal)); + var finalLines = lines + .OrderBy(x => DateTime.ParseExact(x.Split(',')[0], "yyyyMMdd", CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal)) + .ToList(); File.WriteAllLines(finalPath, finalLines); } + private static string SanitizeCsv(string value) + { + if (string.IsNullOrEmpty(value)) + { + return string.Empty; + } + return value.Replace(",", string.Empty).Replace("\r", string.Empty).Replace("\n", string.Empty).Trim(); + } + /// /// Tries to normalize a potentially defunct ticker into a normal ticker. /// @@ -245,25 +323,18 @@ protected static bool TryNormalizeDefunctTicker(string rawTicker, out string[] t tickerList = ticker.Substring(0, length).Trim().Split(' '); return true; } - + tickerList = ticker.Split(' '); return true; } private class RawInsiderTrading : QuiverInsiderTrading { - /// - /// The time the data point ends at and becomes available to the algorithm - /// - [JsonProperty(PropertyName = "Date")] - [JsonConverter(typeof(DateTimeJsonConverter), "yyyy-MM-dd")] - public DateTime Date { get; set; } - - /// - /// The ticker/symbol for the company - /// [JsonProperty(PropertyName = "Ticker")] public string Ticker { get; set; } = null!; + + [JsonProperty(PropertyName = "uploaded")] + public DateTime? Uploaded { get; set; } } } diff --git a/OwnershipType.cs b/OwnershipType.cs new file mode 100644 index 0000000..1a383a3 --- /dev/null +++ b/OwnershipType.cs @@ -0,0 +1,46 @@ +/* + * 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.Runtime.Serialization; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace QuantConnect.DataSource.QuiverQuant +{ + /// + /// SEC Form 4 direct or indirect ownership classification + /// + [JsonConverter(typeof(StringEnumConverter))] + public enum OwnershipType + { + /// + /// Default value used when no ownership flag is provided or the value is unrecognized + /// + [EnumMember(Value = "")] + Unknown, + + /// + /// D - Direct ownership of the security by the reporting person + /// + [EnumMember(Value = "D")] + Direct, + + /// + /// I - Indirect ownership of the security (e.g., through a trust or family member) + /// + [EnumMember(Value = "I")] + Indirect, + } +} diff --git a/QuiverCNBC.cs b/QuiverCNBC.cs index 5555681..c453afd 100644 --- a/QuiverCNBC.cs +++ b/QuiverCNBC.cs @@ -28,8 +28,6 @@ namespace QuantConnect.DataSource /// public class QuiverCNBC : BaseData { - private static readonly TimeSpan _period = TimeSpan.FromDays(1); - /// /// Contract description /// @@ -49,10 +47,15 @@ public class QuiverCNBC : BaseData [JsonProperty(PropertyName = "Traders")] public string Traders { get; set; } + /// + /// Date the trader issued the stock advice on CNBC + /// + public DateTime AdviceDate { get; set; } + /// /// Time the data became available /// - public override DateTime EndTime => Time + _period; + public override DateTime EndTime => Time.AddDays(1); /// /// Parses the data from the line provided and loads it into LEAN @@ -66,16 +69,16 @@ public override BaseData Reader(SubscriptionDataConfig config, string line, Date { var csv = line.Split(','); - var parsedDate = Parse.DateTimeExact(csv[0], "yyyyMMdd"); + var uploadedDate = Parse.DateTimeExact(csv[0], "yyyyMMdd"); return new QuiverCNBC { Symbol = config.Symbol, - Notes = csv[1], - Direction = (OrderDirection)Enum.Parse(typeof(OrderDirection), csv[2], true), + Time = uploadedDate.AddDays(-1), + AdviceDate = (csv[1].IfNotNullOrEmpty(s => Parse.DateTimeExact(s, "yyyyMMdd")) ?? uploadedDate).AddDays(-1), + Direction = QuiverQuant.QuiverQuantCsvExtensions.ToOrderDirection(csv[2]), Traders = csv[3], - - Time = parsedDate + Notes = csv.Length > 4 ? csv[4] : string.Empty, }; } @@ -89,6 +92,7 @@ public override BaseData Clone() { Symbol = Symbol, Time = Time, + AdviceDate = AdviceDate, Notes = Notes, Direction = Direction, Traders = Traders, diff --git a/QuiverCNBCsUniverse.cs b/QuiverCNBCsUniverse.cs index 691d124..5dcc211 100644 --- a/QuiverCNBCsUniverse.cs +++ b/QuiverCNBCsUniverse.cs @@ -25,17 +25,15 @@ namespace QuantConnect.DataSource { /// - /// Universe Selection helper class for QuiverQuant Congress dataset + /// Universe Selection helper class for QuiverQuant CNBC dataset /// public class QuiverCNBCsUniverse : BaseDataCollection { - private static readonly TimeSpan _period = TimeSpan.FromDays(1); - /// /// Extra Information /// public string Notes { get; set; } - + /// /// Direction of trade /// @@ -46,10 +44,15 @@ public class QuiverCNBCsUniverse : BaseDataCollection /// public string Traders { get; set; } + /// + /// Date the trader issued the stock advice on CNBC + /// + public DateTime AdviceDate { get; set; } + /// /// Time the data became available /// - public override DateTime EndTime => Time + _period; + public override DateTime EndTime => Time.AddDays(1); /// /// Return the URL string source of the file. This will be converted to a stream @@ -89,10 +92,11 @@ public override BaseData Reader(SubscriptionDataConfig config, string line, Date return new QuiverCNBCsUniverse { Symbol = new Symbol(SecurityIdentifier.Parse(csv[0]), csv[1]), - Time = date, - Notes = csv[2], - Direction = (OrderDirection)Enum.Parse(typeof(OrderDirection), csv[3], true), + Time = date.AddDays(-1), + AdviceDate = (csv[2].IfNotNullOrEmpty(s => Parse.DateTimeExact(s, "yyyyMMdd")) ?? date).AddDays(-1), + Direction = QuiverQuant.QuiverQuantCsvExtensions.ToOrderDirection(csv[3]), Traders = csv[4], + Notes = csv.Length > 5 ? csv[5] : string.Empty, }; } @@ -150,6 +154,7 @@ public override BaseData Clone() Time = Time, Data = Data, + AdviceDate = AdviceDate, Notes = Notes, Direction = Direction, Traders = Traders diff --git a/QuiverInsiderTrading.cs b/QuiverInsiderTrading.cs index 25ba803..6d1ab07 100644 --- a/QuiverInsiderTrading.cs +++ b/QuiverInsiderTrading.cs @@ -1,4 +1,4 @@ -/* +/* * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. * @@ -18,6 +18,8 @@ using NodaTime; using QuantConnect.Data; using QuantConnect.Data.UniverseSelection; +using QuantConnect.DataSource.QuiverQuant; +using QuantConnect.Util; using System; using System.Collections.Generic; using System.Globalization; @@ -31,36 +33,90 @@ namespace QuantConnect.DataSource [JsonObject] public class QuiverInsiderTrading : BaseDataCollection { - private static readonly TimeSpan _period = TimeSpan.FromDays(1); + /// + /// Transaction date as reported on SEC Form 4 + /// + [JsonProperty(PropertyName = "Date")] + [JsonConverter(typeof(DateTimeJsonConverter), "yyyy-MM-dd")] + public DateTime? Date { get; set; } /// - /// Name + /// Time the transaction was filed and became publicly available /// - [JsonProperty(PropertyName = "Name")] - public string Name { get; set; } + [JsonProperty(PropertyName = "fileDate")] + public DateTime? FileDate { get; set; } /// - /// Shares amount in transaction + /// Type of transaction (see SEC Form 4 codes: + /// https://www.sec.gov/files/forms-3-4-5.pdf) /// - [JsonProperty(PropertyName = "Shares")] - public decimal? Shares { get; set; } + [JsonProperty(PropertyName = "TransactionCode")] + public TransactionCode TransactionCode { get; set; } /// - /// PricePerShare of transaction + /// Reported price per share transacted /// [JsonProperty(PropertyName = "PricePerShare")] public decimal? PricePerShare { get; set; } /// - /// Shares Owned after transcation + /// Number of shares transacted + /// + [JsonProperty(PropertyName = "Shares")] + public decimal? Shares { get; set; } + + /// + /// Number of shares owned by insider following the transaction /// [JsonProperty(PropertyName = "SharesOwnedFollowing")] public decimal? SharesOwnedFollowing { get; set; } + /// + /// Indicates whether transaction was share acquisition or disposal + /// + [JsonProperty(PropertyName = "AcquiredDisposedCode")] + public AcquiredDisposedCode AcquiredDisposedCode { get; set; } + + /// + /// Whether the security is held directly or indirectly by the reporting person + /// + [JsonProperty(PropertyName = "directOrIndirectOwnership")] + public OwnershipType DirectOrIndirectOwnership { get; set; } + + /// + /// Corporate title of the transactor + /// + [JsonProperty(PropertyName = "officerTitle")] + public string OfficerTitle { get; set; } + + /// + /// Whether the transactor is a director of the company + /// + [JsonProperty(PropertyName = "isDirector")] + public bool? IsDirector { get; set; } + + /// + /// Whether the transactor is an officer of the company + /// + [JsonProperty(PropertyName = "isOfficer")] + public bool? IsOfficer { get; set; } + + /// + /// Whether the transactor is a 10% owner of the company + /// + [JsonProperty(PropertyName = "isTenPercentOwner")] + public bool? IsTenPercentOwner { get; set; } + + /// + /// Whether the transactor is not a director, officer, or 10% owner + /// + [JsonProperty(PropertyName = "isOther")] + public bool? IsOther { get; set; } + /// /// The time the data point ends at and becomes available to the algorithm /// - public override DateTime EndTime => Time + _period; + public override DateTime EndTime => Time.AddDays(1); /// /// Return the URL string source of the file. This will be converted to a stream @@ -96,16 +152,25 @@ public override BaseData Reader(SubscriptionDataConfig config, string line, Date { var csv = line.Split(','); - var parsedDate = Parse.DateTimeExact(csv[0], "yyyyMMdd");//, "'yyyy-MM-dd\'T\'HH:mm:ss.SSS\'Z\''" + var uploadedDate = Parse.DateTimeExact(csv[0], "yyyyMMdd"); return new QuiverInsiderTrading { - Name = csv[1], - Shares = csv[2].IfNotNullOrEmpty(s => decimal.Parse(s, NumberStyles.Any, CultureInfo.InvariantCulture)), - PricePerShare = csv[3].IfNotNullOrEmpty(s => decimal.Parse(s, NumberStyles.Any, CultureInfo.InvariantCulture)), - SharesOwnedFollowing = csv[4].IfNotNullOrEmpty(s => decimal.Parse(s, NumberStyles.Any, CultureInfo.InvariantCulture)), - Time = parsedDate, - Symbol = config.Symbol + 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")), + 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]), }; } @@ -119,7 +184,9 @@ public override string ToString() // we are the wrapper instance return $"{Symbol} - Data Points {Data.Count}"; } - return $"{Symbol} - {Name} - {Shares} - {PricePerShare} - {SharesOwnedFollowing}"; + return $"{Symbol} ({OfficerTitle}) - {TransactionCode}/{AcquiredDisposedCode} - " + + $"{Shares} @ {PricePerShare} - SharesOwnedFollowing: {SharesOwnedFollowing} - " + + $"Ownership: {DirectOrIndirectOwnership} - Date: {Date} - Filed: {FileDate}"; } /// @@ -138,10 +205,19 @@ public override BaseData Clone() { return new QuiverInsiderTrading() { - Name = Name, - Shares = Shares, + Date = Date, + FileDate = FileDate, + TransactionCode = TransactionCode, PricePerShare = PricePerShare, + Shares = Shares, SharesOwnedFollowing = SharesOwnedFollowing, + AcquiredDisposedCode = AcquiredDisposedCode, + DirectOrIndirectOwnership = DirectOrIndirectOwnership, + OfficerTitle = OfficerTitle, + IsDirector = IsDirector, + IsOfficer = IsOfficer, + IsTenPercentOwner = IsTenPercentOwner, + IsOther = IsOther, Data = Data, Symbol = Symbol, Time = Time, @@ -180,7 +256,7 @@ public override List SupportedResolutions() /// The of this data type public override DateTimeZone DataTimeZone() { - return DateTimeZone.Utc; + return TimeZones.Utc; } } } diff --git a/QuiverInsiderTradingUniverse.cs b/QuiverInsiderTradingUniverse.cs index bfb3301..57e8228 100644 --- a/QuiverInsiderTradingUniverse.cs +++ b/QuiverInsiderTradingUniverse.cs @@ -21,6 +21,7 @@ using NodaTime; using QuantConnect.Data; using QuantConnect.Data.UniverseSelection; +using QuantConnect.DataSource.QuiverQuant; using static QuantConnect.StringExtensions; namespace QuantConnect.DataSource @@ -30,32 +31,75 @@ namespace QuantConnect.DataSource /// public class QuiverInsiderTradingUniverse : BaseDataCollection { - private static readonly TimeSpan _period = TimeSpan.FromDays(1); + /// + /// Transaction date as reported on SEC Form 4 + /// + public DateTime? Date { get; set; } /// - /// Name + /// Time the transaction was filed and became publicly available /// - public string Name { get; set; } + public DateTime? FileDate { get; set; } /// - /// Shares amount in transaction + /// Type of transaction (SEC Form 4 code) /// - public decimal? Shares { get; set; } + public TransactionCode TransactionCode { get; set; } /// - /// PricePerShare of transaction + /// Reported price per share transacted /// public decimal? PricePerShare { get; set; } /// - /// Shares Owned after transcation + /// Number of shares transacted + /// + public decimal? Shares { get; set; } + + /// + /// Number of shares owned by insider following the transaction /// public decimal? SharesOwnedFollowing { get; set; } /// - /// Time the data became available + /// Indicates whether transaction was share acquisition or disposal + /// + public AcquiredDisposedCode AcquiredDisposedCode { get; set; } + + /// + /// Whether the security is held directly or indirectly + /// + public OwnershipType DirectOrIndirectOwnership { get; set; } + + /// + /// Corporate title of the transactor + /// + public string OfficerTitle { get; set; } + + /// + /// Whether the transactor is a director of the company + /// + public bool? IsDirector { get; set; } + + /// + /// Whether the transactor is an officer of the company + /// + public bool? IsOfficer { get; set; } + + /// + /// Whether the transactor is a 10% owner of the company + /// + public bool? IsTenPercentOwner { get; set; } + + /// + /// Whether the transactor is not a director, officer, or 10% owner + /// + public bool? IsOther { get; set; } + + /// + /// Time the data becomes available to the algorithm /// - public override DateTime EndTime => Time + _period; + public override DateTime EndTime => Time.AddDays(1); /// /// Return the URL string source of the file. This will be converted to a stream @@ -92,19 +136,25 @@ public override BaseData Reader(SubscriptionDataConfig config, string line, Date { var csv = line.Split(','); - var shares = csv[3].IfNotNullOrEmpty(s => decimal.Parse(s, NumberStyles.Any, CultureInfo.InvariantCulture)); - var price = csv[4].IfNotNullOrEmpty(s => decimal.Parse(s, NumberStyles.Any, CultureInfo.InvariantCulture)); - var sharesAfter = csv[5].IfNotNullOrEmpty(s => decimal.Parse(s, NumberStyles.Any, CultureInfo.InvariantCulture)); - + var price = csv[5].IfNotNullOrEmpty(s => decimal.Parse(s, NumberStyles.Any, CultureInfo.InvariantCulture)); + return new QuiverInsiderTradingUniverse { - Time = date, - Name = csv[2], - Shares = shares, - PricePerShare = price, - SharesOwnedFollowing = sharesAfter, - + 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")), + 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]), Value = price ?? 0 }; } @@ -115,10 +165,19 @@ public override BaseData Reader(SubscriptionDataConfig config, string line, Date public override string ToString() { return Invariant($"{Symbol}({Time}) :: ") + - Invariant($"Name: {string.Join(';', Name)} ") + - Invariant($"Shares: {Shares} ") + + Invariant($"Date: {Date} ") + + Invariant($"FileDate: {FileDate} ") + + Invariant($"TransactionCode: {TransactionCode} ") + Invariant($"PricePerShare: {PricePerShare} ") + - Invariant($"SharesOwnedFollowing: {SharesOwnedFollowing}"); + Invariant($"Shares: {Shares} ") + + Invariant($"SharesOwnedFollowing: {SharesOwnedFollowing} ") + + Invariant($"AcquiredDisposedCode: {AcquiredDisposedCode} ") + + Invariant($"DirectOrIndirectOwnership: {DirectOrIndirectOwnership} ") + + Invariant($"OfficerTitle: {OfficerTitle} ") + + Invariant($"IsDirector: {IsDirector} ") + + Invariant($"IsOfficer: {IsOfficer} ") + + Invariant($"IsTenPercentOwner: {IsTenPercentOwner} ") + + Invariant($"IsOther: {IsOther}"); } /// @@ -128,10 +187,19 @@ public override BaseData Clone() { return new QuiverInsiderTradingUniverse() { - Name = Name, - Shares = Shares, + Date = Date, + FileDate = FileDate, + TransactionCode = TransactionCode, PricePerShare = PricePerShare, + Shares = Shares, SharesOwnedFollowing = SharesOwnedFollowing, + AcquiredDisposedCode = AcquiredDisposedCode, + DirectOrIndirectOwnership = DirectOrIndirectOwnership, + OfficerTitle = OfficerTitle, + IsDirector = IsDirector, + IsOfficer = IsOfficer, + IsTenPercentOwner = IsTenPercentOwner, + IsOther = IsOther, Data = Data, Symbol = Symbol, Time = Time, @@ -160,7 +228,7 @@ public override List SupportedResolutions() /// The of this data type public override DateTimeZone DataTimeZone() { - return TimeZones.Chicago; + return TimeZones.Utc; } } -} \ No newline at end of file +} diff --git a/QuiverQuantCsvExtensions.cs b/QuiverQuantCsvExtensions.cs new file mode 100644 index 0000000..1259d7a --- /dev/null +++ b/QuiverQuantCsvExtensions.cs @@ -0,0 +1,128 @@ +/* + * 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 QuantConnect.Orders; + +namespace QuantConnect.DataSource.QuiverQuant +{ + /// + /// Compact CSV serialization helpers for Quiver enums and primitives. Keeps the + /// on-disk format short (single SEC letters, 0/1 booleans, -1/0/1 trade direction) + /// while preserving full enum names in code. + /// + public static class QuiverQuantCsvExtensions + { + public static string ToCsv(this TransactionCode value) => value switch + { + TransactionCode.Sale => "S", + TransactionCode.Purchase => "P", + TransactionCode.VoluntaryReport => "V", + TransactionCode.GrantOrAward => "A", + TransactionCode.DispositionToIssuer => "D", + TransactionCode.ExercisePaymentWithSecurities => "F", + TransactionCode.DiscretionaryTransaction => "I", + TransactionCode.ExerciseOrConversionExempt => "M", + TransactionCode.ConversionOfDerivative => "C", + TransactionCode.ShortDerivativeExpiration => "E", + TransactionCode.LongDerivativeExpirationWithValue => "H", + TransactionCode.OutOfMoneyExercise => "O", + TransactionCode.InMoneyExercise => "X", + TransactionCode.Gift => "G", + TransactionCode.SmallAcquisition => "L", + TransactionCode.AcquisitionByWill => "W", + TransactionCode.VotingTrustDeposit => "Z", + TransactionCode.Other => "J", + TransactionCode.EquitySwap => "K", + TransactionCode.TenderDisposition => "U", + _ => string.Empty, + }; + + public static TransactionCode ToTransactionCode(string value) => value switch + { + "S" => TransactionCode.Sale, + "P" => TransactionCode.Purchase, + "V" => TransactionCode.VoluntaryReport, + "A" => TransactionCode.GrantOrAward, + "D" => TransactionCode.DispositionToIssuer, + "F" => TransactionCode.ExercisePaymentWithSecurities, + "I" => TransactionCode.DiscretionaryTransaction, + "M" => TransactionCode.ExerciseOrConversionExempt, + "C" => TransactionCode.ConversionOfDerivative, + "E" => TransactionCode.ShortDerivativeExpiration, + "H" => TransactionCode.LongDerivativeExpirationWithValue, + "O" => TransactionCode.OutOfMoneyExercise, + "X" => TransactionCode.InMoneyExercise, + "G" => TransactionCode.Gift, + "L" => TransactionCode.SmallAcquisition, + "W" => TransactionCode.AcquisitionByWill, + "Z" => TransactionCode.VotingTrustDeposit, + "J" => TransactionCode.Other, + "K" => TransactionCode.EquitySwap, + "U" => TransactionCode.TenderDisposition, + _ => TransactionCode.Other, + }; + + public static string ToCsv(this OwnershipType value) => value switch + { + OwnershipType.Direct => "D", + OwnershipType.Indirect => "I", + _ => string.Empty, + }; + + public static OwnershipType ToOwnershipType(string value) => value switch + { + "D" => OwnershipType.Direct, + "I" => OwnershipType.Indirect, + _ => OwnershipType.Unknown, + }; + + public static string ToCsv(this AcquiredDisposedCode value) => value switch + { + AcquiredDisposedCode.Acquired => "A", + AcquiredDisposedCode.Disposed => "D", + _ => string.Empty, + }; + + public static AcquiredDisposedCode ToAcquiredDisposedCode(string value) => value switch + { + "A" => AcquiredDisposedCode.Acquired, + "D" => AcquiredDisposedCode.Disposed, + _ => AcquiredDisposedCode.Unknown, + }; + + public static string ToCsv(this bool? value) => value switch + { + true => "T", + false => "F", + null => string.Empty, + }; + + public static bool? ToNullableBool(string value) => value switch + { + "T" => true, + "F" => false, + _ => null, + }; + + public static string ToCsv(this OrderDirection value) => ((int)value).ToString(System.Globalization.CultureInfo.InvariantCulture); + + public static OrderDirection ToOrderDirection(string value) + { + return int.TryParse(value, System.Globalization.NumberStyles.Integer, System.Globalization.CultureInfo.InvariantCulture, out var parsed) + ? (OrderDirection)parsed + : OrderDirection.Hold; + } + } +} diff --git a/TransactionCode.cs b/TransactionCode.cs new file mode 100644 index 0000000..ddf95a6 --- /dev/null +++ b/TransactionCode.cs @@ -0,0 +1,150 @@ +/* + * 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.Runtime.Serialization; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace QuantConnect.DataSource.QuiverQuant +{ + /// + /// SEC Form 4 transaction codes (see https://www.sec.gov/files/forms-3-4-5.pdf) + /// + [JsonConverter(typeof(StringEnumConverter))] + public enum TransactionCode + { + /// + /// S - Open market or private sale of non-derivative or derivative security + /// + [EnumMember(Value = "S")] + Sale = -1, + + /// + /// J - Other acquisition or disposition (describe transaction). Also used as the + /// default value when no transaction code is provided. + /// + [EnumMember(Value = "J")] + Other, + + /// + /// P - Open market or private purchase of non-derivative or derivative security + /// + [EnumMember(Value = "P")] + Purchase, + + /// + /// V - Transaction voluntarily reported earlier than required + /// + [EnumMember(Value = "V")] + VoluntaryReport, + + /// + /// A - Grant, award, or other acquisition pursuant to Rule 16b-3(d) + /// + [EnumMember(Value = "A")] + GrantOrAward, + + /// + /// D - Disposition to the issuer of issuer equity securities pursuant to Rule 16b-3(e) + /// + [EnumMember(Value = "D")] + DispositionToIssuer, + + /// + /// F - Payment of exercise price or tax liability by delivering or withholding securities + /// incident to the receipt, exercise, or vesting of a security issued in accordance with Rule 16b-3 + /// + [EnumMember(Value = "F")] + ExercisePaymentWithSecurities, + + /// + /// I - Discretionary transaction in accordance with Rule 16b-3(f) + /// + [EnumMember(Value = "I")] + DiscretionaryTransaction, + + /// + /// M - Exercise or conversion of derivative security exempted pursuant to Rule 16b-3 + /// + [EnumMember(Value = "M")] + ExerciseOrConversionExempt, + + /// + /// C - Conversion of derivative security + /// + [EnumMember(Value = "C")] + ConversionOfDerivative, + + /// + /// E - Expiration of short derivative position + /// + [EnumMember(Value = "E")] + ShortDerivativeExpiration, + + /// + /// H - Expiration (or cancellation) of long derivative position with value received + /// + [EnumMember(Value = "H")] + LongDerivativeExpirationWithValue, + + /// + /// O - Exercise of out-of-the-money derivative security + /// + [EnumMember(Value = "O")] + OutOfMoneyExercise, + + /// + /// X - Exercise of in-the-money or at-the-money derivative security + /// + [EnumMember(Value = "X")] + InMoneyExercise, + + /// + /// G - Bona fide gift + /// + [EnumMember(Value = "G")] + Gift, + + /// + /// L - Small acquisition under Rule 16a-6 + /// + [EnumMember(Value = "L")] + SmallAcquisition, + + /// + /// W - Acquisition or disposition by will or the laws of descent and distribution + /// + [EnumMember(Value = "W")] + AcquisitionByWill, + + /// + /// Z - Deposit into or withdrawal from voting trust + /// + [EnumMember(Value = "Z")] + VotingTrustDeposit, + + /// + /// K - Transaction in equity swap or instrument with similar characteristics + /// + [EnumMember(Value = "K")] + EquitySwap, + + /// + /// U - Disposition pursuant to a tender of shares in a change of control transaction + /// + [EnumMember(Value = "U")] + TenderDisposition, + } +} diff --git a/tests/QuiverCNBCTests.cs b/tests/QuiverCNBCTests.cs index a458529..95083a8 100644 --- a/tests/QuiverCNBCTests.cs +++ b/tests/QuiverCNBCTests.cs @@ -17,6 +17,7 @@ using System; using System.Linq; using Newtonsoft.Json; +using NodaTime; using NUnit.Framework; using QuantConnect.Data; using QuantConnect.DataSource; @@ -57,6 +58,82 @@ public void CloneCollection() AssertAreEqual(expected, result); } + [Test] + public void Reader_ParsesCompactFormat() + { + var symbol = new Symbol(SecurityIdentifier.Parse("AAPL R735QTJ8XC9X"), "AAPL"); + var config = CreateConfig(symbol); + var factory = new QuiverCNBC(); + // csv: uploadDate, adviceDate, direction(0=Buy/1=Sell/2=Hold), traders, notes + var line = "20260508,20260507,0,Jim Cramer,catalyst"; + + var result = (QuiverCNBC)factory.Reader(config, line, new DateTime(2026, 5, 8), false); + + Assert.AreEqual(symbol, result.Symbol); + Assert.AreEqual(new DateTime(2026, 5, 7), result.Time); + Assert.AreEqual(new DateTime(2026, 5, 8), result.EndTime); + Assert.AreEqual(new DateTime(2026, 5, 6), result.AdviceDate); + Assert.AreEqual(OrderDirection.Buy, result.Direction); + Assert.AreEqual("Jim Cramer", result.Traders); + Assert.AreEqual("catalyst", result.Notes); + } + + [Test] + public void Reader_EmptyAdviceDateFallsBackToUploadedMinusOne() + { + var symbol = new Symbol(SecurityIdentifier.Parse("AAPL R735QTJ8XC9X"), "AAPL"); + var config = CreateConfig(symbol); + var factory = new QuiverCNBC(); + var line = "20260508,,1,Steve Weiss,"; + + var result = (QuiverCNBC)factory.Reader(config, line, new DateTime(2026, 5, 8), false); + + Assert.AreEqual(new DateTime(2026, 5, 7), result.Time); + Assert.AreEqual(new DateTime(2026, 5, 7), result.AdviceDate); + Assert.AreEqual(OrderDirection.Sell, result.Direction); + Assert.AreEqual(string.Empty, result.Notes); + } + + [Test] + public void Reader_MissingTrailingNotesDefaultsToEmpty() + { + var symbol = new Symbol(SecurityIdentifier.Parse("AAPL R735QTJ8XC9X"), "AAPL"); + var config = CreateConfig(symbol); + var factory = new QuiverCNBC(); + // 4 columns only — trailing notes column missing entirely + var line = "20260508,20260507,2,Rob Sechan"; + + var result = (QuiverCNBC)factory.Reader(config, line, new DateTime(2026, 5, 8), false); + + Assert.AreEqual(OrderDirection.Hold, result.Direction); + Assert.AreEqual("Rob Sechan", result.Traders); + Assert.AreEqual(string.Empty, result.Notes); + } + + [Test] + public void UniverseReader_ParsesCompactFormat() + { + var factory = new QuiverCNBCsUniverse(); + // csv: sid, ticker, adviceDate, direction, traders, notes + var line = "AAPL R735QTJ8XC9X,AAPL,20260507,0,Jim Cramer,catalyst"; + + var result = (QuiverCNBCsUniverse)factory.Reader(null, line, new DateTime(2026, 5, 8), false); + + Assert.AreEqual("AAPL", result.Symbol.Value); + Assert.AreEqual(new DateTime(2026, 5, 7), result.Time); + Assert.AreEqual(new DateTime(2026, 5, 6), result.AdviceDate); + Assert.AreEqual(OrderDirection.Buy, result.Direction); + Assert.AreEqual("Jim Cramer", result.Traders); + Assert.AreEqual("catalyst", result.Notes); + } + + private static SubscriptionDataConfig CreateConfig(Symbol symbol) + { + return new SubscriptionDataConfig( + typeof(QuiverCNBC), symbol, Resolution.Daily, + DateTimeZone.Utc, DateTimeZone.Utc, false, false, false); + } + private void AssertAreEqual(object expected, object result, bool filterByCustomAttributes = false) { foreach (var propertyInfo in expected.GetType().GetProperties()) diff --git a/tests/QuiverInsiderTradingTests.cs b/tests/QuiverInsiderTradingTests.cs index 44370a9..104fa14 100644 --- a/tests/QuiverInsiderTradingTests.cs +++ b/tests/QuiverInsiderTradingTests.cs @@ -17,10 +17,12 @@ using System; using System.Linq; using Newtonsoft.Json; +using NodaTime; using NUnit.Framework; using QuantConnect.Data; using QuantConnect.DataProcessing; using QuantConnect.DataSource; +using QuantConnect.DataSource.QuiverQuant; namespace QuantConnect.DataLibrary.Tests { @@ -47,6 +49,98 @@ public void Clone() AssertAreEqual(expected, result); } + [Test] + 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 result = (QuiverInsiderTrading)factory.Reader(config, line, new DateTime(2026, 5, 8), false); + + Assert.AreEqual(symbol, result.Symbol); + Assert.AreEqual(new DateTime(2026, 5, 7), result.Time); + Assert.AreEqual(new DateTime(2026, 5, 8), result.EndTime); + Assert.AreEqual(new DateTime(2026, 5, 7), result.Date); + Assert.AreEqual(new DateTime(2026, 5, 6, 9, 30, 0), result.FileDate); + Assert.AreEqual(TransactionCode.Purchase, result.TransactionCode); + Assert.AreEqual(150.25m, result.PricePerShare); + Assert.AreEqual(100m, result.Shares); + Assert.AreEqual(500m, result.SharesOwnedFollowing); + Assert.AreEqual(AcquiredDisposedCode.Acquired, result.AcquiredDisposedCode); + Assert.AreEqual(OwnershipType.Direct, result.DirectOrIndirectOwnership); + Assert.AreEqual("CEO", result.OfficerTitle); + Assert.AreEqual(true, result.IsDirector); + Assert.AreEqual(true, result.IsOfficer); + Assert.AreEqual(false, result.IsTenPercentOwner); + Assert.IsNull(result.IsOther); + } + + [Test] + public void Reader_EmptyFileDateFallsBackToUploadedMinusOne() + { + var symbol = new Symbol(SecurityIdentifier.Parse("AAPL R735QTJ8XC9X"), "AAPL"); + 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 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_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 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.OfficerTitle); + Assert.IsNull(result.IsDirector); + Assert.IsNull(result.IsOfficer); + Assert.IsNull(result.IsTenPercentOwner); + Assert.IsNull(result.IsOther); + } + + [Test] + 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 result = (QuiverInsiderTradingUniverse)factory.Reader(null, line, new DateTime(2026, 5, 8), false); + + Assert.AreEqual("AAPL", result.Symbol.Value); + Assert.AreEqual(new DateTime(2026, 5, 7), result.Time); + Assert.AreEqual(new DateTime(2026, 5, 7), result.FileDate); + Assert.AreEqual(new DateTime(2026, 5, 7), result.Date); + Assert.AreEqual(TransactionCode.Purchase, result.TransactionCode); + Assert.AreEqual(150.25m, result.PricePerShare); + Assert.AreEqual(AcquiredDisposedCode.Acquired, result.AcquiredDisposedCode); + Assert.AreEqual(OwnershipType.Direct, result.DirectOrIndirectOwnership); + Assert.AreEqual(150.25m, result.Value); + } + + private static SubscriptionDataConfig CreateConfig(Symbol symbol) + { + return new SubscriptionDataConfig( + typeof(QuiverInsiderTrading), symbol, Resolution.Daily, + DateTimeZone.Utc, DateTimeZone.Utc, false, false, false); + } + [TestCase("abc123:msft\"", ExpectedResult = new string[] {"MSFT"})] [TestCase("AAPL+", ExpectedResult = new string[] {"AAPL"})] [TestCase("AAPL-", ExpectedResult = new string[] {"AAPL"})] @@ -84,10 +178,19 @@ private BaseData CreateNewInstance() Symbol = Symbol.Empty, Time = DateTime.Today, DataType = MarketDataType.Base, - Name = "Institution name", + Date = DateTime.Today, + FileDate = DateTime.Today, + TransactionCode = TransactionCode.Purchase, Shares = 0.0m, PricePerShare = 0.0m, - SharesOwnedFollowing = 0.0m + SharesOwnedFollowing = 0.0m, + AcquiredDisposedCode = AcquiredDisposedCode.Acquired, + DirectOrIndirectOwnership = OwnershipType.Direct, + OfficerTitle = "CEO", + IsDirector = false, + IsOfficer = true, + IsTenPercentOwner = false, + IsOther = false, }; } diff --git a/tests/QuiverQuantCsvExtensionsTests.cs b/tests/QuiverQuantCsvExtensionsTests.cs new file mode 100644 index 0000000..3749123 --- /dev/null +++ b/tests/QuiverQuantCsvExtensionsTests.cs @@ -0,0 +1,120 @@ +/* + * 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 NUnit.Framework; +using QuantConnect.DataSource.QuiverQuant; +using QuantConnect.Orders; + +namespace QuantConnect.DataLibrary.Tests +{ + [TestFixture] + public class QuiverQuantCsvExtensionsTests + { + [TestCase(TransactionCode.Sale, "S")] + [TestCase(TransactionCode.Purchase, "P")] + [TestCase(TransactionCode.VoluntaryReport, "V")] + [TestCase(TransactionCode.GrantOrAward, "A")] + [TestCase(TransactionCode.DispositionToIssuer, "D")] + [TestCase(TransactionCode.ExercisePaymentWithSecurities, "F")] + [TestCase(TransactionCode.DiscretionaryTransaction, "I")] + [TestCase(TransactionCode.ExerciseOrConversionExempt, "M")] + [TestCase(TransactionCode.ConversionOfDerivative, "C")] + [TestCase(TransactionCode.ShortDerivativeExpiration, "E")] + [TestCase(TransactionCode.LongDerivativeExpirationWithValue, "H")] + [TestCase(TransactionCode.OutOfMoneyExercise, "O")] + [TestCase(TransactionCode.InMoneyExercise, "X")] + [TestCase(TransactionCode.Gift, "G")] + [TestCase(TransactionCode.SmallAcquisition, "L")] + [TestCase(TransactionCode.AcquisitionByWill, "W")] + [TestCase(TransactionCode.VotingTrustDeposit, "Z")] + [TestCase(TransactionCode.Other, "J")] + [TestCase(TransactionCode.EquitySwap, "K")] + [TestCase(TransactionCode.TenderDisposition, "U")] + public void TransactionCode_RoundTrip(TransactionCode value, string expectedLetter) + { + Assert.AreEqual(expectedLetter, value.ToCsv()); + Assert.AreEqual(value, QuiverQuantCsvExtensions.ToTransactionCode(expectedLetter)); + } + + [TestCase("", TransactionCode.Other)] + [TestCase("?", TransactionCode.Other)] + [TestCase("unknown", TransactionCode.Other)] + public void TransactionCode_UnknownInputFallsBackToOther(string input, TransactionCode expected) + { + Assert.AreEqual(expected, QuiverQuantCsvExtensions.ToTransactionCode(input)); + } + + [TestCase(OwnershipType.Direct, "D")] + [TestCase(OwnershipType.Indirect, "I")] + [TestCase(OwnershipType.Unknown, "")] + public void OwnershipType_RoundTrip(OwnershipType value, string expectedLetter) + { + Assert.AreEqual(expectedLetter, value.ToCsv()); + Assert.AreEqual(value, QuiverQuantCsvExtensions.ToOwnershipType(expectedLetter)); + } + + [TestCase("?", OwnershipType.Unknown)] + public void OwnershipType_UnknownInputFallsBackToUnknown(string input, OwnershipType expected) + { + Assert.AreEqual(expected, QuiverQuantCsvExtensions.ToOwnershipType(input)); + } + + [TestCase(AcquiredDisposedCode.Acquired, "A")] + [TestCase(AcquiredDisposedCode.Disposed, "D")] + [TestCase(AcquiredDisposedCode.Unknown, "")] + public void AcquiredDisposedCode_RoundTrip(AcquiredDisposedCode value, string expectedLetter) + { + Assert.AreEqual(expectedLetter, value.ToCsv()); + Assert.AreEqual(value, QuiverQuantCsvExtensions.ToAcquiredDisposedCode(expectedLetter)); + } + + [TestCase("?", AcquiredDisposedCode.Unknown)] + public void AcquiredDisposedCode_UnknownInputFallsBackToUnknown(string input, AcquiredDisposedCode expected) + { + Assert.AreEqual(expected, QuiverQuantCsvExtensions.ToAcquiredDisposedCode(input)); + } + + [TestCase(true, "T")] + [TestCase(false, "F")] + [TestCase(null, "")] + public void NullableBool_RoundTrip(bool? value, string expected) + { + Assert.AreEqual(expected, value.ToCsv()); + Assert.AreEqual(value, QuiverQuantCsvExtensions.ToNullableBool(expected)); + } + + [TestCase("anything", null)] + public void NullableBool_UnknownInputFallsBackToNull(string input, bool? expected) + { + Assert.AreEqual(expected, QuiverQuantCsvExtensions.ToNullableBool(input)); + } + + [TestCase(OrderDirection.Buy, "0")] + [TestCase(OrderDirection.Sell, "1")] + [TestCase(OrderDirection.Hold, "2")] + public void OrderDirection_RoundTrip(OrderDirection value, string expected) + { + Assert.AreEqual(expected, value.ToCsv()); + Assert.AreEqual(value, QuiverQuantCsvExtensions.ToOrderDirection(expected)); + } + + [TestCase("", OrderDirection.Hold)] + [TestCase("xyz", OrderDirection.Hold)] + public void OrderDirection_UnparsableInputFallsBackToHold(string input, OrderDirection expected) + { + Assert.AreEqual(expected, QuiverQuantCsvExtensions.ToOrderDirection(input)); + } + } +} From 1e2bb09fe0fc96e3fd6c25a1af4203aa9be639dc Mon Sep 17 00:00:00 2001 From: Alexandre Catarino Date: Tue, 12 May 2026 10:20:50 +0100 Subject: [PATCH 2/2] Select dataset via config + fix path layout for Congress / WallStreetBets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Program.cs now reads the dataset name from the `vendor-data-name` config key (default `cnbc`) instead of args, lifts `processingDate` and `processingStartDate` to the top so every case shares them, and prints the valid options when an unknown dataset is provided. - QuiverCongressDataDownloader now combines `destinationFolder` with `VendorName` and `VendorDataName`, matching every other downloader. Previously it dropped `VendorName` from the path. - QuiverWallStreetBetsDataDownloader stops re-prefixing `alternative` — Program.cs already passes `destinationDirectory` (which includes it), so the downloader now follows the same convention as the others. - config.json gains `processing-date-lookback` and `vendor-data-name` defaults; auth token kept empty. Co-Authored-By: Claude Opus 4.7 (1M context) --- DataProcessing/Program.cs | 56 ++++++++++--------- .../QuiverCongressDataDownloader.cs | 2 +- .../QuiverWallStreetBetsDataDownloader.cs | 2 +- DataProcessing/config.json | 4 +- 4 files changed, 34 insertions(+), 30 deletions(-) diff --git a/DataProcessing/Program.cs b/DataProcessing/Program.cs index 0c3d0b7..0582238 100644 --- a/DataProcessing/Program.cs +++ b/DataProcessing/Program.cs @@ -26,13 +26,14 @@ namespace QuantConnect.DataProcessing /// public class Program { + private static readonly string VendorName = QuiverDataDownloader.VendorName; /// /// Entrypoint of the program /// /// Exit code. 0 equals successful, and any other value indicates the downloader/converter failed. - public static void Main(string[] args) + public static void Main() { - var dataset = args.Length > 0 ? args[0].ToLowerInvariant() : "cnbc"; + var dataset = Config.Get("vendor-data-name", "cnbc").Trim().ToLowerInvariant(); var destinationDirectory = Path.Combine( Config.Get("temp-output-directory", "/temp-output-directory"), "alternative"); @@ -45,12 +46,11 @@ public static void Main(string[] args) var processingDateLookback = Config.GetInt("processing-date-lookback", 0); var processingStartDate = processingDate.AddDays(-processingDateLookback); - switch (dataset.ToLowerInvariant()) + switch (dataset) { - case "cnbc": + case QuiverCNBCDataDownloader.VendorDataName: { RunDownloader( - QuiverDataDownloader.VendorName, QuiverCNBCDataDownloader.VendorDataName, () => new QuiverCNBCDataDownloader(destinationDirectory, processedDataDirectory), instance => @@ -60,7 +60,7 @@ public static void Main(string[] args) if (!instance.Run(date)) { Log.Error($"QuantConnect.DataProcessing.Program.Main(): Failed to download/process " + - $"{QuiverDataDownloader.VendorName} {QuiverCNBCDataDownloader.VendorDataName} data for date: {date:yyyy-MM-dd}"); + $"{VendorName} {QuiverCNBCDataDownloader.VendorDataName} data for date: {date:yyyy-MM-dd}"); } } instance.Flush(); @@ -70,7 +70,7 @@ public static void Main(string[] args) break; } - case "governmentcontract": + case QuiverGovernmentContractDownloader.VendorDataName: { var datasetStartDate = new DateTime(2022, 4, 21); if (processingDate < datasetStartDate) @@ -80,7 +80,6 @@ public static void Main(string[] args) } RunDownloader( - QuiverDataDownloader.VendorName, QuiverGovernmentContractDownloader.VendorDataName, () => new QuiverGovernmentContractDownloader(), instance => @@ -89,7 +88,7 @@ public static void Main(string[] args) if (!success) { Log.Error($"QuantConnect.DataProcessing.Program.Main(): Failed to download/process " + - $"{QuiverDataDownloader.VendorName} {QuiverGovernmentContractDownloader.VendorDataName} data for date: {processingDate:yyyy-MM-dd}"); + $"{VendorName} {QuiverGovernmentContractDownloader.VendorDataName} data for date: {processingDate:yyyy-MM-dd}"); } instance.ProcessUniverse(); return true; @@ -97,42 +96,36 @@ public static void Main(string[] args) break; } - case "lobbying": + case QuiverLobbyingDataDownloader.VendorDataName: { RunDownloader( - QuiverDataDownloader.VendorName, QuiverLobbyingDataDownloader.VendorDataName, () => new QuiverLobbyingDataDownloader(destinationDirectory, processedDataDirectory), instance => instance.Run(processingDate)); break; } - case "congresstrading": + case QuiverCongressDataDownloader.VendorDataName: { - var congressDestination = Path.Combine(destinationDirectory, "quiver"); RunDownloader( - QuiverDataDownloader.VendorName, QuiverCongressDataDownloader.VendorDataName, - () => new QuiverCongressDataDownloader(congressDestination), + () => new QuiverCongressDataDownloader(destinationDirectory), instance => instance.Run()); break; } - case "wallstreetbets": + case QuiverWallStreetBetsDataDownloader.VendorDataName: { - var tempOutput = Config.Get("temp-output-directory", "/temp-output-directory"); RunDownloader( - QuiverDataDownloader.VendorName, QuiverWallStreetBetsDataDownloader.VendorDataName, - () => new QuiverWallStreetBetsDataDownloader(tempOutput), + () => new QuiverWallStreetBetsDataDownloader(destinationDirectory), instance => instance.Run()); break; } - case "insidertrading": + case QuiverInsiderTradingDataDownloader.VendorDataName: { RunDownloader( - QuiverDataDownloader.VendorName, QuiverInsiderTradingDataDownloader.VendorDataName, () => new QuiverInsiderTradingDataDownloader(destinationDirectory, processedDataDirectory), instance => @@ -142,7 +135,7 @@ public static void Main(string[] args) if (!instance.Run(date)) { Log.Error($"QuantConnect.DataProcessing.Program.Main(): Failed to download/process " + - $"{QuiverDataDownloader.VendorName} {QuiverInsiderTradingDataDownloader.VendorDataName} data for date: {date:yyyy-MM-dd}"); + $"{VendorName} {QuiverInsiderTradingDataDownloader.VendorDataName} data for date: {date:yyyy-MM-dd}"); } } instance.Flush(); @@ -153,14 +146,23 @@ public static void Main(string[] args) } default: - Log.Error($"Unknown dataset '{dataset}'"); + var validDatasets = string.Join(", ", new[] + { + QuiverCNBCDataDownloader.VendorDataName, + QuiverGovernmentContractDownloader.VendorDataName, + QuiverLobbyingDataDownloader.VendorDataName, + QuiverCongressDataDownloader.VendorDataName, + QuiverWallStreetBetsDataDownloader.VendorDataName, + QuiverInsiderTradingDataDownloader.VendorDataName, + }); + Log.Error($"Unknown dataset '{dataset}'. Valid options: {validDatasets}"); break; } Environment.Exit(0); } - private static void RunDownloader(string vendorName, string vendorDataName, Func factory, Func run) where T : class, IDisposable + private static void RunDownloader(string vendorDataName, Func factory, Func run) where T : class, IDisposable { T instance = null; try @@ -169,7 +171,7 @@ private static void RunDownloader(string vendorName, string vendorDataName, F } catch (Exception err) { - Log.Error(err, $"QuantConnect.DataProcessing.Program.Main(): The downloader/converter for {vendorName} {vendorDataName} data failed to be constructed"); + Log.Error(err, $"QuantConnect.DataProcessing.Program.Main(): The downloader/converter for {VendorName} {vendorDataName} data failed to be constructed"); Environment.Exit(1); } @@ -177,13 +179,13 @@ private static void RunDownloader(string vendorName, string vendorDataName, F { if (!run(instance)) { - Log.Error($"QuantConnect.DataProcessing.Program.Main(): Failed to download/process {vendorName} {vendorDataName} data"); + Log.Error($"QuantConnect.DataProcessing.Program.Main(): Failed to download/process {VendorName} {vendorDataName} data"); Environment.Exit(1); } } catch (Exception err) { - Log.Error(err, $"QuantConnect.DataProcessing.Program.Main(): The downloader/converter for {vendorName} {vendorDataName} data exited unexpectedly"); + Log.Error(err, $"QuantConnect.DataProcessing.Program.Main(): The downloader/converter for {VendorName} {vendorDataName} data exited unexpectedly"); Environment.Exit(1); } finally diff --git a/DataProcessing/QuiverCongressDataDownloader.cs b/DataProcessing/QuiverCongressDataDownloader.cs index 87ca5db..7323e4c 100644 --- a/DataProcessing/QuiverCongressDataDownloader.cs +++ b/DataProcessing/QuiverCongressDataDownloader.cs @@ -57,7 +57,7 @@ public class QuiverCongressDataDownloader : QuiverDataDownloader public QuiverCongressDataDownloader(string destinationFolder, string apiKey = null) : base(100, TimeSpan.FromSeconds(60), apiKey) { - _destinationFolder = Directory.CreateDirectory(Path.Combine(destinationFolder, VendorDataName)).FullName; + _destinationFolder = Directory.CreateDirectory(Path.Combine(destinationFolder, VendorName, VendorDataName)).FullName; _universeFolder = Directory.CreateDirectory(Path.Combine(_destinationFolder, "universe")).FullName; } diff --git a/DataProcessing/QuiverWallStreetBetsDataDownloader.cs b/DataProcessing/QuiverWallStreetBetsDataDownloader.cs index 97e1844..8b1b3b5 100644 --- a/DataProcessing/QuiverWallStreetBetsDataDownloader.cs +++ b/DataProcessing/QuiverWallStreetBetsDataDownloader.cs @@ -46,7 +46,7 @@ public class QuiverWallStreetBetsDataDownloader : QuiverDataDownloader public QuiverWallStreetBetsDataDownloader(string destinationFolder, string apiKey = null) : base(10, TimeSpan.FromSeconds(1.1), apiKey) { - _destinationFolder = Path.Combine(destinationFolder, "alternative", VendorName, VendorDataName); + _destinationFolder = Path.Combine(destinationFolder, VendorName, VendorDataName); _universeFolder = Path.Combine(_destinationFolder, "universe"); Directory.CreateDirectory(_destinationFolder); diff --git a/DataProcessing/config.json b/DataProcessing/config.json index 65f5a10..fdb03d4 100644 --- a/DataProcessing/config.json +++ b/DataProcessing/config.json @@ -1,4 +1,6 @@ { "data-folder": "../../Lean/Data/", - "quiver-auth-token": "" + "quiver-auth-token": "", + "processing-date-lookback": 0, + "vendor-data-name": "cnbc" }