Files
quantconnect--lean/ToolBox/CoarseUniverseGenerator/CoarseUniverseGeneratorProgram.cs
2019-04-26 20:39:27 +00:00

388 lines
19 KiB
C#

/*
* 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;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
using Ionic.Zip;
using Newtonsoft.Json.Linq;
using QuantConnect.Data.Auxiliary;
using QuantConnect.Interfaces;
using QuantConnect.Util;
using Log = QuantConnect.Logging.Log;
namespace QuantConnect.ToolBox.CoarseUniverseGenerator
{
public static class CoarseUniverseGeneratorProgram
{
private const string ExclusionsFile = "exclusions.txt";
/// <summary>
/// This program generates the coarse files requires by lean for universe selection.
/// Universe selection is planned to happen in two stages, the first stage, the 'coarse'
/// stage serves to cull the set using coarse filters, such as price, market, and dollar volume.
/// Later we'll support full fundamental data such as ratios and financial statements, and these
/// would be run AFTER the initial coarse filter
///
/// The files are generated from LEAN formatted daily trade bar equity files
/// </summary>
public static void CoarseUniverseGenerator()
{
// read out the configuration file
JToken jtoken;
var config = JObject.Parse(File.ReadAllText("CoarseUniverseGenerator/config.json"));
var ignoreMaplessSymbols = false;
var updateMode = false;
var updateTime = TimeSpan.Zero;
DateTime? startDate = null;
if (config.TryGetValue("update-mode", out jtoken))
{
updateMode = jtoken.Value<bool>();
if (config.TryGetValue("update-time-of-day", out jtoken))
{
updateTime = TimeSpan.Parse(jtoken.Value<string>());
}
}
var dataDirectory = Globals.DataFolder;
if (config.TryGetValue("data-directory", out jtoken))
{
dataDirectory = jtoken.Value<string>();
}
//Ignore symbols without a map file:
// Typically these are nothing symbols (NASDAQ test symbols, or symbols listed for a few days who aren't actually ever traded).
if (config.TryGetValue("ignore-mapless", out jtoken))
{
ignoreMaplessSymbols = jtoken.Value<bool>();
}
do
{
ProcessEquityDirectories(dataDirectory, ignoreMaplessSymbols);
}
while (WaitUntilTimeInUpdateMode(updateMode, updateTime));
}
/// <summary>
/// If we're in update mode, pause the thread until the next update time
/// </summary>
/// <param name="updateMode">True for update mode, false for run-once</param>
/// <param name="updateTime">The time of day updates should be performed</param>
/// <returns>True if in update mode, otherwise false</returns>
private static bool WaitUntilTimeInUpdateMode(bool updateMode, TimeSpan updateTime)
{
if (!updateMode) return false;
var now = DateTime.Now;
var timeUntilNextProcess = (now.Date.AddDays(1).Add(updateTime) - now);
Thread.Sleep((int)timeUntilNextProcess.TotalMilliseconds);
return true;
}
/// <summary>
/// Iterates over each equity directory and aggregates the data into the coarse file
/// </summary>
/// <param name="dataDirectory">The Lean /Data directory</param>
/// <param name="ignoreMaplessSymbols">Ignore symbols without a QuantQuote map file.</param>
public static IEnumerable<string> ProcessEquityDirectories(string dataDirectory, bool ignoreMaplessSymbols)
{
var exclusions = ReadExclusionsFile(ExclusionsFile);
var equity = Path.Combine(dataDirectory, "equity");
foreach (var directory in Directory.EnumerateDirectories(equity))
{
var dailyFolder = Path.Combine(directory, "daily");
var mapFileFolder = Path.Combine(directory, "map_files");
var coarseFolder = Path.Combine(directory, "fundamental", "coarse");
if (!Directory.Exists(coarseFolder))
{
Directory.CreateDirectory(coarseFolder);
}
var factorFileProvider = new LocalDiskFactorFileProvider();
var files = ProcessDailyFolder(dailyFolder, coarseFolder, MapFileResolver.Create(mapFileFolder), factorFileProvider, exclusions, ignoreMaplessSymbols);
foreach (var file in files)
{
yield return file;
}
}
}
/// <summary>
/// Iterates each daily file in the specified <paramref name="dailyFolder"/> and adds a line for each
/// day to the appropriate coarse file
/// </summary>
/// <param name="dailyFolder">The folder with daily data.</param>
/// <param name="coarseFolder">The coarse output folder.</param>
/// <param name="mapFileResolver">The map file resolver.</param>
/// <param name="factorFileProvider">The factor file provider.</param>
/// <param name="exclusions">The symbols to be excluded from processing.</param>
/// <param name="ignoreMapless">Ignore the symbols without a map file.</param>
/// <param name="symbolResolver">Function used to provide symbol resolution. Default resolution uses the zip file name to resolve
/// the symbol, specify null for this behavior.</param>
/// <returns>Collection with the names of the newly generated coarse files.</returns>
/// <exception cref="Exception">
/// Unable to resolve market for daily folder: " + dailyFolder
/// or
/// Unable to resolve fundamental path for coarse folder: " + coarseFolder
/// </exception>
public static ICollection<string> ProcessDailyFolder(string dailyFolder, string coarseFolder, MapFileResolver mapFileResolver, IFactorFileProvider factorFileProvider,
HashSet<string> exclusions, bool ignoreMapless, Func<string, string> symbolResolver = null)
{
const decimal scaleFactor = 10000m;
Log.Trace("Processing: {0}", dailyFolder);
var start = DateTime.UtcNow;
// load map files into memory
var symbols = 0;
var maplessCount = 0;
var dates = new HashSet<DateTime>();
// instead of opening/closing these constantly, open them once and dispose at the end (~3x speed improvement)
var writers = new Dictionary<string, StreamWriter>();
var marketDirectoryInfo = new DirectoryInfo(dailyFolder).Parent;
if (marketDirectoryInfo == null)
{
throw new Exception("Unable to resolve market for daily folder: " + dailyFolder);
}
var market = marketDirectoryInfo.Name.ToLower();
var fundamentalDirectoryInfo = new DirectoryInfo(coarseFolder).Parent;
if (fundamentalDirectoryInfo == null)
{
throw new Exception("Unable to resolve fundamental path for coarse folder: " + coarseFolder);
}
var fineFundamentalFolder = Path.Combine(marketDirectoryInfo.FullName, "fundamental", "fine");
// open up each daily file to get the values and append to the daily coarse files
foreach (var file in Directory.EnumerateFiles(dailyFolder, "*.zip"))
{
try
{
var ticker = Path.GetFileNameWithoutExtension(file);
var fineAvailableDates = Enumerable.Empty<DateTime>();
var tickerFineFundamentalFolder = Path.Combine(fineFundamentalFolder, ticker);
if (Directory.Exists(tickerFineFundamentalFolder))
{
fineAvailableDates = Directory.GetFiles(tickerFineFundamentalFolder, "*.zip")
.Select(f => DateTime.ParseExact(Path.GetFileNameWithoutExtension(f), DateFormat.EightCharacter, CultureInfo.InvariantCulture))
.ToList();
}
if (ticker == null)
{
Log.Trace("CoarseGenerator.ProcessDailyFolder(): Unable to resolve symbol from file: {0}", file);
continue;
}
if (symbolResolver != null)
{
ticker = symbolResolver(ticker);
}
ticker = ticker.ToUpper();
if (exclusions != null && exclusions.Contains(ticker))
{
Log.Trace("Excluded symbol: {0}", ticker);
continue;
}
ZipFile zip;
using (var reader = Compression.Unzip(file, out zip))
{
var checkedForMapFile = false;
symbols++;
string line;
while ((line = reader.ReadLine()) != null)
{
//20150625.csv
var csv = line.Split(',');
var date = DateTime.ParseExact(csv[0], DateFormat.TwelveCharacter, CultureInfo.InvariantCulture);
if (ignoreMapless && !checkedForMapFile)
{
checkedForMapFile = true;
if (!mapFileResolver.ResolveMapFile(ticker, date).Any())
{
// if the resolved map file has zero entries then it's a mapless symbol
maplessCount++;
break;
}
}
var close = decimal.Parse(csv[4]) / scaleFactor;
var volume = long.Parse(csv[5]);
var dollarVolume = close * volume;
var coarseFile = Path.Combine(coarseFolder, date.ToString("yyyyMMdd") + ".csv");
dates.Add(date);
// try to resolve a map file and if found, regen the sid
var sid = SecurityIdentifier.GenerateEquity(SecurityIdentifier.DefaultDate, ticker, market);
var mapFile = mapFileResolver.ResolveMapFile(ticker, date);
if (!mapFile.IsNullOrEmpty())
{
// if available, us the permtick in the coarse files, because of this, we need
// to update the coarse files each time new map files are added/permticks change
sid = SecurityIdentifier.GenerateEquity(mapFile.FirstDate, mapFile.OrderBy(x => x.Date).First().MappedSymbol, market);
}
if (mapFile == null && ignoreMapless)
{
// if we're ignoring mapless files then we should always be able to resolve this
Log.Error(string.Format("CoarseGenerator.ProcessDailyFolder(): Unable to resolve map file for {0} as of {1}", ticker, date.ToShortDateString()));
continue;
}
// get price and split factors from factor files
var symbol = new Symbol(sid, ticker);
var factorFile = factorFileProvider.Get(symbol);
var factorFileRow = factorFile?.GetScalingFactors(date);
var priceFactor = factorFileRow?.PriceFactor ?? 1m;
var splitFactor = factorFileRow?.SplitFactor ?? 1m;
// Check if security has fine file within a trailing month for a date-ticker set.
// There are tricky cases where a folder named by a ticker can have data for multiple securities.
// e.g GOOG -> GOOGL (GOOG T1AZ164W5VTX) / GOOCV -> GOOG (GOOCV VP83T1ZUHROL) case.
// The fine data in the 'fundamental/fine/goog' folder will be for 'GOOG T1AZ164W5VTX' up to the 2014-04-02 and for 'GOOCV VP83T1ZUHROL' afterward.
// Therefore, date before checking if the security has fundamental data for a date, we need to filter the fine files the map's first date.
var firstDate = mapFile?.FirstDate ?? DateTime.MinValue;
var hasFundamentalDataForDate = fineAvailableDates.Where(d => d >= firstDate).Any(d => date.AddMonths(-1) <= d && d <= date);
// The following section handles mergers and acquisitions cases.
// e.g. YHOO -> AABA (YHOO R735QTJ8XC9X)
// The dates right after the acquisition, valid fine fundamental data for AABA are still under the former ticker folder.
// Therefore if no fine fundamental data is found in the 'fundamental/fine/aaba' folder, it searches into the 'yhoo' folder.
if (mapFile != null && mapFile.Count() > 2 && !hasFundamentalDataForDate)
{
var previousTicker = mapFile.LastOrDefault(m => m.Date < date)?.MappedSymbol;
if (previousTicker != null)
{
var previousTickerFineFundamentalFolder = Path.Combine(fineFundamentalFolder, previousTicker);
if (Directory.Exists(previousTickerFineFundamentalFolder))
{
var previousTickerFineAvailableDates = Directory.GetFiles(previousTickerFineFundamentalFolder, "*.zip")
.Select(f => DateTime.ParseExact(Path.GetFileNameWithoutExtension(f), DateFormat.EightCharacter, CultureInfo.InvariantCulture))
.ToList();
hasFundamentalDataForDate = previousTickerFineAvailableDates.Where(d => d >= firstDate).Any(d => date.AddMonths(-1) <= d && d <= date);
}
}
}
// sid,symbol,close,volume,dollar volume,has fundamental data,price factor,split factor
var coarseFileLine = $"{sid},{ticker},{close},{volume},{Math.Truncate(dollarVolume)},{hasFundamentalDataForDate},{priceFactor},{splitFactor}";
StreamWriter writer;
if (!writers.TryGetValue(coarseFile, out writer))
{
writer = new StreamWriter(new FileStream(coarseFile, FileMode.Create, FileAccess.Write, FileShare.Write));
writers[coarseFile] = writer;
}
writer.WriteLine(coarseFileLine);
}
}
if (symbols % 1000 == 0)
{
Log.Trace("CoarseGenerator.ProcessDailyFolder(): Completed processing {0} symbols. Current elapsed: {1} seconds", symbols, (DateTime.UtcNow - start).TotalSeconds.ToString("0.00"));
}
}
catch (Exception err)
{
// log the error and continue with the process
Log.Error(err.ToString());
}
}
Log.Trace("CoarseGenerator.ProcessDailyFolder(): Saving {0} coarse files to disk", dates.Count);
// dispose all the writers at the end of processing
foreach (var writer in writers)
{
writer.Value.Dispose();
}
var stop = DateTime.UtcNow;
Log.Trace("CoarseGenerator.ProcessDailyFolder(): Processed {0} symbols into {1} coarse files in {2} seconds", symbols, dates.Count, (stop - start).TotalSeconds.ToString("0.00"));
Log.Trace("CoarseGenerator.ProcessDailyFolder(): Excluded {0} mapless symbols.", maplessCount);
return writers.Keys;
}
/// <summary>
/// Reads the specified exclusions file into a new hash set.
/// Returns an empty set if the file does not exist
/// </summary>
public static HashSet<string> ReadExclusionsFile(string exclusionsFile)
{
var exclusions = new HashSet<string>();
if (File.Exists(exclusionsFile))
{
var excludedSymbols = File.ReadLines(exclusionsFile).Select(x => x.Trim()).Where(x => !x.StartsWith("#"));
exclusions = new HashSet<string>(excludedSymbols, StringComparer.InvariantCultureIgnoreCase);
Log.Trace("CoarseGenerator.ReadExclusionsFile(): Loaded {0} symbols into the exclusion set", exclusions.Count);
}
return exclusions;
}
/// <summary>
/// Resolves the start date that should be used in the <see cref="ProcessDailyFolder"/>. This will
/// be equal to the latest file date (20150101.csv) plus one day
/// </summary>
/// <param name="coarseDirectory">The directory containing the coarse files</param>
/// <returns>The last coarse file date plus one day if exists, else DateTime.MinValue</returns>
public static DateTime GetLastProcessedDate(string coarseDirectory)
{
var lastProcessedDate = (
from coarseFile in Directory.EnumerateFiles(coarseDirectory)
let date = TryParseCoarseFileDate(coarseFile)
where date != null
// we'll start on the following day
select date.Value.AddDays(1)
).DefaultIfEmpty(DateTime.MinValue).Max();
return lastProcessedDate;
}
private static DateTime? TryParseCoarseFileDate(string coarseFile)
{
try
{
var dateString = Path.GetFileNameWithoutExtension(coarseFile);
return DateTime.ParseExact(dateString, "yyyyMMdd", null);
}
catch
{
return null;
}
}
}
}