/*
* 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 NAMESPACES
**********************************************************/
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Reflection;
using Fasterflect;
using QuantConnect.Securities;
using QuantConnect.Logging;
using QuantConnect.AlgorithmFactory;
using QuantConnect.Data;
namespace QuantConnect.Lean.Engine
{
/********************************************************
* CLASS DEFINITIONS
*********************************************************/
///
/// Subscription data reader is a wrapper on the stream reader class to download, unpack and iterate over a data file.
///
/// The class accepts any subscription configuration and automatically makes it availble to enumerate
public class SubscriptionDataReader : IEnumerator
{
/********************************************************
* CLASS PRIVATE VARIABLES
*********************************************************/
/// Source string to create memory stream:
private string _source = "";
///Default true to fillforward for this subscription, take the previous result and continue returning it till the next time barrier.
private bool _isFillForward = true;
///Date of this source file.
private DateTime _date = new DateTime();
///End of stream from the reader
private bool _endOfStream = false;
/// Internal stream reader for processing data line by line:
private SubscriptionStreamReader _reader = null;
/// All streams done async via web protocols:
private WebClient _web = new WebClient();
/// Configuration of the data-reader:
private SubscriptionDataConfig _config;
/// Subscription Securities Access
private Security _security;
/// Save security type of speed
private bool _isQCEquity = false;
// Subscription is for a QC type:
private bool _isQCData = false;
//Price Factor Mapping:
private SortedDictionary _priceFactors;
private decimal _priceFactor = 0;
//Symbol Mapping:
private SortedDictionary _symbolMap;
private string _mappedSymbol = "";
/// Location of the datafeed - the type of this data.
private DataFeedEndpoint _feedEndpoint = DataFeedEndpoint.Backtesting;
/// Object Activator - Fast create new instance of "Type":
private Func _objectActivator;
///Create a single instance to invoke all Type Methods:
private BaseData _dataFactory;
/// Access to Reader Method:
private MethodInfo _readerMethod;
///FastReflect Method Invoker
private MethodInvoker _readerMethodInvoker;
/// Access to Get Source Method:
private MethodInfo _getSourceMethod;
/// Remember edge conditions as market enters/leaves open-closed.
private BaseData _lastBarOfStream = null;
private BaseData _lastBarOutsideMarketHours = null;
//Start finish times of the backtest:
private DateTime _periodStart;
private DateTime _periodFinish;
/********************************************************
* CLASS PUBLIC VARIABLES
*********************************************************/
///
/// Last read BaseData object from this type and source
///
public BaseData Current
{
get;
private set;
}
///
/// Explicit Interface Implementation for Current
///
object IEnumerator.Current
{
get { throw new NotImplementedException(); }
}
///
/// Save an instance of the previous basedata we generated
///
public BaseData Previous
{
get;
private set;
}
///
/// Source has been completed, load up next stream or stop asking for data.
///
public bool EndOfStream
{
get
{
return _endOfStream || _reader == null;
}
}
///
/// Simple flag to show if this is a QC data type
///
public bool IsQCTick
{
get;
private set;
}
///
/// Simple flag to show if this is a QC data type
///
public bool IsQCTradeBar
{
get;
private set;
}
/********************************************************
* CLASS CONSTRUCTOR
*********************************************************/
///
/// Subscription data reader takes a subscription request, loads the type, accepts the data source and enumerate on the results.
///
/// Subscription configuration object
/// Security asset
/// Feed type enum
/// Start date for the data request/backtest
/// Finish date for the data request/backtest
public SubscriptionDataReader(SubscriptionDataConfig config, Security security, DataFeedEndpoint feed, DateTime periodStart, DateTime periodFinish)
{
//Save configuration of data-subscription:
_config = config;
//Save access to fill foward flag:
_isFillForward = config.FillDataForward;
//Save Start and End Dates:
_periodStart = periodStart;
_periodFinish = periodFinish;
//Save access to securities
_security = security;
_isQCData = security.IsQuantConnectData;
_isQCEquity = (security.Type == SecurityType.Equity) && _isQCData;
//Set QC Type Flags:
IsQCTick = (config.Type.Name == "Tick" && _isQCData);
IsQCTradeBar = (config.Type.Name == "TradeBar" && _isQCData);
//Save the type of data we'll be getting from the source.
_feedEndpoint = feed;
//Create the dynamic type-activators:
_objectActivator = Loader.GetActivator(config.Type);
if (_objectActivator == null)
{
Engine.ResultHandler.ErrorMessage("Custom data type '" + config.Type.Name + "' missing parameterless constructor E.g. public " + config.Type.Name + "() { }");
_endOfStream = true;
return;
}
//Create an instance of the "Type":
var userObj = _objectActivator.Invoke(new object[] { });
_dataFactory = userObj as BaseData;
//Save Access to the "Reader" Method:
_readerMethod = _dataFactory.GetType().GetMethod("Reader", new[] { typeof(SubscriptionDataConfig), typeof(string), typeof(DateTime), typeof(DataFeedEndpoint) });
//Create a Delagate Accessor.
_readerMethodInvoker = _readerMethod.DelegateForCallMethod();
//Save access to the "GetSource" Method:
_getSourceMethod = _dataFactory.GetType().GetMethod("GetSource", new[] { typeof(SubscriptionDataConfig), typeof(DateTime), typeof(DataFeedEndpoint) });
//Load the entire factor and symbol mapping tables into memory
try
{
if (_isQCEquity)
{
_priceFactors = SubscriptionAdjustment.GetFactorTable(config.Symbol);
_symbolMap = SubscriptionAdjustment.GetMapTable(config.Symbol);
}
}
catch (Exception err)
{
Log.Error("SubscriptionDataReader(): Fetching Price/Map Factors: " + err.Message);
_priceFactors = new SortedDictionary();
_symbolMap = new SortedDictionary();
}
}
/********************************************************
* CLASS METHODS
*********************************************************/
///
/// Try and create a new instance of the object and return it using the MoveNext enumeration pattern ("Current" public variable).
///
/// This is a highly called method and should be kept lean as possible.
/// Boolean true on successful move next. Set Current public property.
public bool MoveNext() {
BaseData instance = null;
var instanceMarketOpen = false;
//Log.Debug("SubscriptionDataReader.MoveNext(): Starting MoveNext...");
//Calls this when no file, first "moveNext()" in refresh source.
if (_endOfStream || _reader == null || _reader.EndOfStream)
{
if (_reader == null)
{
//Handle the 1% of time:: getReader failed (e.g. missing day on S3) so skip day:
Current = null;
}
else
{
//This is a MoveNext() after reading the last line of file:
_lastBarOfStream = Current;
}
_endOfStream = true;
return false;
}
//Log.Debug("SubscriptionDataReader.MoveNext(): Launching While-InstanceNotNull && not EOS: " + reader.EndOfStream);
//Keep looking until output's an instance:
while (instance == null && !_reader.EndOfStream)
{
//Get the next string line from file, create instance of BaseData:
var line = _reader.ReadLine();
try
{
//Using Fasterflex method invokers.
instance = _readerMethodInvoker(_dataFactory, _config, line, _date, _feedEndpoint) as BaseData;
}
catch (Exception err)
{
//Log.Debug("SubscriptionDataReader.MoveNext(): Error invoking instance: " + err.Message);
Engine.ResultHandler.RuntimeError("Error invoking " + _config.Symbol + " data reader. Line: " + line + " Error: " + err.Message, err.StackTrace);
_endOfStream = true;
}
if (instance != null)
{
instanceMarketOpen = _security.Exchange.DateTimeIsOpen(instance.Time);
//Apply custom user data filters:
try
{
if (!_security.DataFilter.Filter(_security, instance))
{
instance = null;
continue;
}
}
catch (Exception err)
{
Log.Error("SubscriptionDataReader.MoveNext(): Error applying filter: " + err.Message);
Engine.ResultHandler.RuntimeError("Runtime error applying data filter. Assuming filter pass: " + err.Message, err.StackTrace);
}
if (instance == null)
{
Log.Trace("SubscriptionDataReader.MoveNext(): Instance null, continuing...");
continue;
}
//Check if we're in date range of the data request
if (!_isQCData)
{
if (instance.Time < _periodStart)
{
_lastBarOutsideMarketHours = instance;
instance = null;
continue;
}
if (instance.Time > _periodFinish)
{
instance = null;
continue;
}
}
//Save bar for extended market hours (fill forward).
if (!instanceMarketOpen)
{
_lastBarOutsideMarketHours = instance;
}
//However, if we only want market hours data, don't return yet: Discard and continue looping.
if (!_config.ExtendedMarketHours && !instanceMarketOpen)
{
instance = null;
}
}
}
//Handle edge conditions: First Bar Read:
// -> Use previous bar from yesterday if available
if (Current == null)
{
//Handle first loop where not set yet:
if (_lastBarOfStream == null)
{
//For first bar, fill forward from premarket data where possible
_lastBarOfStream = _lastBarOutsideMarketHours ?? instance;
}
//If current not set yet, set Previous to yesterday/last bar read.
Previous = _lastBarOfStream;
}
else
{
Previous = Current;
}
Current = instance;
//End of Stream: rewind reader to last
if (_reader.EndOfStream && instance == null)
{
//Log.Debug("SubscriptionDataReader.MoveNext(): Reader EOS.");
_endOfStream = true;
if (_isFillForward && Previous != null)
{
//If instance == null, current is null, so clone previous to record the final sample:
Current = Previous.Clone();
//When market closes fastforward current bar to the last bar fill forwarded to close time.
Current.Time = _security.Exchange.TimeOfDayClosed(Previous.Time);
// Save the previous bar as last bar before next stream (for fill forwrd).
_lastBarOfStream = Previous;
}
return false;
}
return true;
}
///
/// For backwards adjusted data the price is adjusted by a scale factor which is a combination of splits and dividends.
/// This backwards adjusted price is used by default and fed as the current price.
///
/// Current date of the backtest.
private void UpdateScaleFactors(DateTime date) {
try
{
_mappedSymbol = SubscriptionAdjustment.GetMappedSymbol(_symbolMap, date);
_priceFactor = SubscriptionAdjustment.GetTimePriceFactor(_priceFactors, date);
}
catch (Exception err)
{
Log.Error("SubscriptionDataReader.UpdateScaleFactors(): " + err.Message);
}
_config.SetPriceScaleFactor(_priceFactor);
_config.SetMappedSymbol(_mappedSymbol);
}
///
/// Check if this time is open for this subscription.
///
/// Date and time we're checking to see if the market is open
/// Boolean true on market open
public bool MarketOpen(DateTime time)
{
return _security.Exchange.DateTimeIsOpen(time);
}
///
/// Check if we're still in the extended market hours
///
/// Time to scan
/// True on extended market hours
public bool ExtendedMarketOpen(DateTime time)
{
return _security.Exchange.DateTimeIsExtendedOpen(time);
}
///
/// Reset the IEnumeration
///
/// Not used
public void Reset()
{
throw new NotImplementedException("Reset method not implemented. Assumes loop will only be used once.");
}
///
/// Fetch and set the location of the data from the user's BaseData factory:
///
/// Date of the source file.
/// Boolean true on successfully retrieving the data
public bool RefreshSource(DateTime date)
{
//Update the source from the getSource method:
_date = date;
var newSource = "";
//If this is an equity, apply the backward price scaling (backtesting only).
if (_isQCEquity && (_feedEndpoint == DataFeedEndpoint.Backtesting || _feedEndpoint == DataFeedEndpoint.FileSystem)) {
UpdateScaleFactors(date);
}
//Make sure this particular security is trading today:
if (!_security.Exchange.DateIsOpen(date))
{
_endOfStream = true;
return false;
}
//Choose the new source file, hide the QC source file locations
if (_isQCData)
{
newSource = GetQuantConnectSource(date);
}
else
{
//If its not a QC bar, load the source from the user function.
newSource = GetSource(date);
}
//When stream over stop looping on this data.
if (newSource == "")
{
_endOfStream = true;
return false;
}
//Log.Debug("SubscriptionDataReader.MoveNext(): Source Refresh: " + newSource);
if (_source != newSource && newSource != "")
{
//If a new file, reset the EOS flag:
_endOfStream = false;
//Set the new source.
_source = newSource;
//Close out the last source file.
Dispose();
//Load the source:
try
{
//Log.Debug("SubscriptionDataReader.RefreshSource(): Created new reader for source: " + source);
_reader = GetReader(_source, (_isQCData && !Engine.IsLocal));
}
catch (Exception err)
{
Log.Error("SubscriptionDataReader.RefreshSource(): Failed to get reader: " + err.Message);
//Engine.ResultHandler.DebugMessage("Failed to get a reader for the data source. There may be an error in your custom data source reader. Skipping date (" + date.ToShortDateString() + "). Err: " + err.Message);
return false;
}
if (_reader == null)
{
Log.Error("Failed to get StreamReader for data source(" + _source + "), symbol(" + _mappedSymbol + "). Skipping date(" + date.ToShortDateString() + "). Reader is null.");
//Engine.ResultHandler.DebugMessage("We could not find the requested data. This may be an invalid data request, failed download of custom data, or a public holiday. Skipping date (" + date.ToShortDateString() + ").");
if (!_isQCData)
{
Engine.ResultHandler.ErrorMessage("We could not fetch the requested data. This may not be valid data, or a failed download of custom data. Skipping source (" + _source + ").");
}
return false;
}
//Reset the public properties so we can explicitly set them with lastBar data.
Current = null;
Previous = null;
//99% of time, populate the first "Current". 1% of of time no source file (getReader fails), so
// method sets the Subscription properties as if no data.
try
{
MoveNext();
}
catch (Exception err)
{
throw new Exception("SubscriptionDataReader.RefreshSource(): Could not MoveNext to init stream: " + _source + " " + err.Message + " >> " + err.StackTrace);
}
}
//Success:
return true;
}
///
/// Using this source URL, download it to our cache and open a local reader.
///
/// Source URL for the data:
/// Boolean if this is a QC managed data file, if so can look in QC store
/// StreamReader for the data source
private SubscriptionStreamReader GetReader(string source, bool qcFile = false)
{
//Prepare local folders:
const string cache = "./cache/data";
SubscriptionStreamReader reader = null;
if (!Directory.Exists(cache)) Directory.CreateDirectory(cache);
foreach (var file in Directory.EnumerateFiles(cache)) File.Delete(file);
//1. Download this source file as fast as possible:
//1.1 Create filename from source:
var filename = source.ToMD5() + source.GetExtension();
var location = cache + @"/" + filename;
//1.2 Based on Endpoint, Download File (Backtest) or directly open SR of source:
switch (_feedEndpoint)
{
case DataFeedEndpoint.FileSystem:
case DataFeedEndpoint.Backtesting:
//1.2 Download from source to location
if (qcFile)
{
if (!File.Exists(source))
{
Log.Trace("SubscriptionDataReader.GetReader(): Could not find QC Data, skipped: " + source);
Engine.ResultHandler.SamplePerformance(_date.Date, 0);
return reader;
}
//Data source is also location of raw file; root /data directory.
location = source;
}
else
{
//If the file already exists a
try
{
using (var client = new WebClient())
{
client.DownloadFile(source, location);
}
}
catch (Exception err)
{
Engine.ResultHandler.ErrorMessage("Error downloading custom data source file, skipped: " + source + " Err: " + err.Message, err.StackTrace);
if (OS.IsWindows) Engine.ResultHandler.SamplePerformance(_date.Date, 0);
return reader;
}
}
//2. File downloaded. Open Stream:
if (File.Exists(location))
{
if (source.GetExtension() == ".zip")
{
//Extracting zip returns stream reader:
var sr = Compression.Unzip(location);
if (sr == null) return null;
reader = new SubscriptionStreamReader(sr, _feedEndpoint);
}
else
{
//Custom file stream: open from disk
reader = new SubscriptionStreamReader(location, _feedEndpoint);
}
}
break;
//Directly open for REST Requests:
case DataFeedEndpoint.Tradier:
case DataFeedEndpoint.LiveTrading:
reader = new SubscriptionStreamReader(source, _feedEndpoint);
break;
}
return reader;
}
///
/// Stream the file over the net directly from its source.
///
/// Source URL for the file
/// Left here for potential future reference instead of downloading files we stream then from external source.
/// StreamReader Interface for the data source.
private StreamReader WebReader(string source)
{
//Initialize Required Variables for Web Reader:
StreamReader reader;
//Reopen the source with the new URL.
_web = new WebClient();
using (var stream = _web.OpenRead(source))
{
//If its a zip, unzip it:
if (source.GetExtension() == ".zip")
{
reader = Compression.UnzipStream(stream);
}
else
{
reader = new StreamReader(stream);
}
}
return reader;
}
///
/// Dispose of the Stream Reader and close out the source stream and file connections.
///
public void Dispose()
{
if (_reader != null)
{
_reader.Close();
_reader.Dispose();
}
if (_web != null)
{
_web.Dispose();
}
}
///
/// Get the source URL string for this datetime from the users GetSource() method in BaseData.
///
/// DateTime we're requesting.
/// URL string of the source file
public string GetSource(DateTime date)
{
var newSource = "";
//Invoke our instance of this method.
if (_dataFactory != null)
{
try
{
newSource = _getSourceMethod.Invoke(_dataFactory, new object[] { _config, date, _feedEndpoint }) as String;
}
catch (Exception err)
{
Log.Error("SubscriptionDataReader.GetSource(): " + err.Message);
Engine.ResultHandler.ErrorMessage("Error getting string source location for custom data source: " + err.Message, err.StackTrace);
}
}
//Return the freshly calculated source URL.
return newSource;
}
///
/// Get the source location of this QuantConnect data request.
///
/// Date to retrieve
/// string source
public string GetQuantConnectSource(DateTime date)
{
var source = "";
var dataType = TickType.Trade;
switch (_feedEndpoint)
{
//BAcktesting S3 Endpoint:
case DataFeedEndpoint.Backtesting:
case DataFeedEndpoint.FileSystem:
var dateFormat = "yyyyMMdd";
if (_config.Security == SecurityType.Forex)
{
dataType = TickType.Quote;
dateFormat = "yyMMdd";
}
source = @"../../../Data/" + _config.Security.ToString().ToLower();
source += @"/" + _config.Resolution.ToString().ToLower() + @"/" + _config.Symbol.ToLower() + @"/";
source += date.ToString(dateFormat) + "_" + dataType.ToString().ToLower() + ".zip";
break;
//Live Trading Endpoint: Fake, not actually used but need for consistency with backtesting system. Set to "" so will not use subscription reader.
case DataFeedEndpoint.Tradier:
case DataFeedEndpoint.LiveTrading:
source = "";
break;
}
return source;
}
} // End Base Data Class
} // End QC Namespace