eb1181f5f7
* Adds preliminary universe selection for Future Options
* Fixes scaling issues with Future Options
* Fixes scaling multiplying by 10000x instead of using _scaleFactor
* Fixes scaling for Tick
* Revert changes to Tick since it divides the scaling factor
* Changes stale method name to new method name after rebase
* Fixes selection bugs, adds new methods, and adds unit tests
* Fixes bug where Equity Symbol was created for an underlying
non-equity Symbol, resulting in equity data trying to be loaded
* Adds unit tests covering changes to Tick, QuoteBar, TradeBar and
LeanData
* Adds regression test for AddUniverseOption filter contract selection
for Future Options
* Addresses review - modifies the AddFutureOption signature
* Adds new AddUniverseOptions method overload
* Removes and adds a new unit test
* Misc. modifications to account for new changes
* Fixes bug where futures were loaded using default SID Date
* Refactors and removes unnecessary work
* Fixes regression algorithm, which previously made no trades
* Adds future option data
* Adds the corresponding underlying data, in this case, futures data
to enable usage of future options data
* Replaces data with new data (ES18Z20)
* Improves Future chain filtering and updates regression stats
* Add AddFutureOptionContract API
* Expands regression and unit tests to test in finer detail
* Adds Python regression algorithms for AddFutureOption[Contract] methods
* Adds new unit test for BacktestingOptionChainProvider
* Fixes bug with BacktesingOptionChainProvider where we
attempted to load the Trades option chain first, resulting
in breakage of backwards compatibility and limitation of the
option chain.
* Adds new regression algorithms (Py) to Algorithm.Python project
* Adds FutureOptionMarginBuyingPowerModel
* Modifies code paths used to select margin model
* Adds related unit tests for margin model
* Fixes issue with unit test and MHDB/SPDB lookup for Future Options
* Preliminary regression algorithm testing ITM call/put option buying
* Fixes bug where fee model used did not find non-US market
options fee model. We now use the futures fee model for future
options because IB charges the same commissions per contract
between futures and futures options
* Adds proper regression algorithm for ITM future options expiration
* Pushing broken algorithm for review
* Currently, algorithm does not fill forward, causing
a single future option to not get exercised when it is delisted.
* Adds FutureOptionPutITMExpiryRegressionAlgorithm
* Improves existing regression algorithm for call side
* Fixes bug in existing regression algorithm
* Adds AAPL daily data to advance enumerator for ^^^ fix
* Adds additional future option regression algorithms
* Adds Buy OTM expiration regression algorithms
* Adds Sell ITM/OTM expiration regression algorithms
* Adds missing Python regression algorithms
* Adds remaining Python regression algorithms and fixes issues
* Fixes naming issues and statistics
* Adds short option OTM regression algorithms (Py)
* Add license header and class comments to python algorithms
* Cleans up comments and docstrings
* Create Buy/Sell call intraday regression algo
* Redirects future options symbol properties to futures symbol properties
* Asserts exercise/assignment price and updates stats in regression algos
* Adds new unit test covering changes to SecurityService
* Adds comments and fixes failing test
* Partially fixes future option mis-calculated profit/loss
* Adjusts portfolio model to calculate FOP as a no upfront pay asset class
* Updates regression algorithm statistics
* Begin IB FOP support
* Initial support for FOP IB data streaming, live í¾
* Adds additional functionality to LiveOptionChainProvider
- Allows querying CME API to retrieve option chains for CME products
- Ultimately, it's also the groundwork for the CME
LiveFutureChainProvider
* Edits IDataQueueUniverseProvider interface to provide greater
control to implementors of it
* Misc. bug fixes required to get FOP data streaming through IB
* Adds comments, adds missing rategate call, and cleans up code
* Force exchange for FOP and Futures when no exchange is provided
* Fixes bug with Portfolio modeling across all asset classes
* Adds LiveOptionChainProvider tests for Future Options
* IB brokerage option symbol bug fixes and improvements
* Fixes contract multiplier lookup bug
* Fixes issue where we attempted to subscribe to IB data feed with canonical security
* Adds ES MHDB entry
* Reverts portfolio modeling changes for Futures Options
* Since IB eats into our account's cash balance when
a new FOP contract is purchased, we must model by applying funds
to our cash whenever a new purchase/sell occurs.
If we choose to model FOPs exactly as we do with futures, we
will end up with an invalid TotalPortfolioValue on algorithm
restart. By all means and purposes, FOPs are modeled exactly
the same as equity options with respect to the portfolio.
* Adds comments clarifying portfolio modeling and clarifies
existing portfolio modeling comments with additional context.
* Fixes IB symbol lookup for future options
* Fixes LiveOptionChainProvider looping 5 times per option chain
request, even on success
* Sets OptionChainedUniverseSelectionModel to produce a canonical
future/future option/option Symbol to avoid creating two Symbols
* Adds GLOBEX future option symbol mapping from future -> fop
* Fixes LiveOptionChainProvider loading wrong contract option chains
* Fixes loading of futures options ZIP files when backtesting
* Adds a string -> decimal JSON converter
* Additional fixes/refactoring to the LiveOptionChainProvider
* Adds tests for changes to Symbol and LeanData
* Reverts changes to IB-symbol-map
* Fixes Value for mapped future options tickers
* Fixes Symbol test
* Changes path of future options to future's expiry date
* Extra changes made to remove scaling from writing CSV
* Added method to map from FOP Globex -> FUT Globex
* Fixes MOO and MOC orders for future options
* Note: this order type might not be supported by IB or CME.
* Bug fixes and updates unit tests
* Update regression tests and data format
* Rebase changes
* 1. Multiple bug fixes for LiveOptionChainProvider, reverts IQFeed changes
2. Address review (partial): Code reuse and cleanup
1.
* Modifies check in
`AddFutureOptionShort(Call|Put)ITMExpiryRegressionAlgorithm`
to ensure no buys have negative quantity
* Code reuse changes in IB brokerage
* Bug fix in IB brokerage where we assigned the FOP expiry
as the futures expiry (requires verification)
* Doc changes and adds missing summaries/license banners
* Disposes of HTTP client resources in LiveOptionChainProvider
* Renames classes and adds FutureOption folder in Common/Securities
2.
* We revert back to the quotes API for the option chain,
since the settlement API sometimes had missing strikes.
* Fixes future option expiry being set as future's expiry
in LiveOptionChainProvider
* Fixes bug where wrong option chain was selected because of bad
expiry lookup in the futures expiries returned from CME
* Fixes multiple looping bug in LiveOptionChainProvider
* Adds strike price scaling for LiveOptionChainProvider
* Reverts IQFeed changes and simplifies interface upgrade changes
Some additional challenges we'll have to solve as part of FOPs:
- The `OptionSymbol.IsStandard` method makes the assumption that
weeklies contracts follow the pattern equities follows, which
does not apply to Futures Options
- The Subscription created in:
`OptionChainUniverseSubscriptionEnumeratorFactory`
...adds a Trade config. For illiquid contracts, this
will delay universe selection for the option symbol
until we get a trade. However, if we add a quote config,
the data would instead be loaded based on the first quote
we received from the brokerage.
But since we're currently using a trade config, illiquid
contracts won't start streaming data until it receives a trade.
NOTE: this commit is a WIP to addressing the reviews received in the PR,
but has been committed early for efficiency in the review process
* Fixes regression algorithms and misc. bugs
* Fixes map file lookup for non-equity options
* Adds extra assertion at end of algorithm to ensure no holdings are
left when the algorithm ends.
* Adds FutureOptionSymbol, allowing all contracts through as standard
* Changes SPDB to allow defaulting to underlying future symbol
properties if no entry is found for the given FOP
* Fixes calls to SPDB in SecurityService, IBBrokerage
* Reverts AAPL daily ZIP file to fix majority of regression algorithms
* Adds FOPs symbol properties
* Fixes existing symbol properties for a few futures
* Adds tests for changes to Symbol Properties Database
* Removes string SPDB lookup method
* Updates tests and misc callees of previous method
* Updates all regression tests to use data of already expired contracts
* Adds Futures Options Expiry Functions tests
* Adds required futures data for 2020-01-05
* Address review (partial): Expands test coverage and fixes tests
* Set option chain tests parallelism to fixture only
* Fixes broken test for contract month delta for FuturesOptionsExpiryFunctions
* Changes delisting date logic for Futures Options
* Address review: removes duplicate code, misc code fixes
* Bug fix in MarketHoursDatabase.GetDatabaseSymbolKey() where
we would use the underlying's Symbol for lookup in the MHDB
* Adds missing license banner
* Removes Futures Options entries from MHDB
* Adds new tests
* Adds SecurityType.FutureOption
* Converts any underlying comparisons and uses SecurityType directly
instead for FOP specific behavior
* Extra code modifications to acommodate new SecurityType
* Addresses review: fixes order fee bug on exercise
* Additional bug fixes and adding of SecurityType.FutureOption
* Updates regression algorithms OrderListHash
* Fixes various bugs in IB live implementation
* Fixes bug setting the right contract expiration date for FOP
generated by LiveOptionChainProvider
* Adds new function to FuturesOptionsExpiryFunctions
* Clarifies parameter names better in some functions/methods
* Fixes bugs in IB brokerage for FOPs
* Address review - code cleanup and refactor
* Remove MappingEventProvider, SplitEventProvider, and
DividendEventProvider for Futures Options in
CorporateEventEnumeratorFactory
* Address review: Use MHDB key resolver in SPDB
* Makes regression tests pass and adds comment for expiry issue
* Fixes MHDB lookup on string symbol method
* Adds Futures Options greeks regression algorithm (C# only)
* Adds explanitory comment on MHDB FOP lookup
* Remove python from FutureOptionCallITMGreeksExpiryRegressionAlgorithm
667 lines
28 KiB
C#
667 lines
28 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;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using QuantConnect.Configuration;
|
|
using QuantConnect.Data;
|
|
using QuantConnect.Data.Auxiliary;
|
|
using QuantConnect.Data.Custom;
|
|
using QuantConnect.Data.Custom.Fred;
|
|
using QuantConnect.Data.Custom.Tiingo;
|
|
using QuantConnect.Interfaces;
|
|
using QuantConnect.Lean.Engine.DataFeeds.Enumerators;
|
|
using QuantConnect.Logging;
|
|
using QuantConnect.Securities.Option;
|
|
|
|
namespace QuantConnect.Lean.Engine.DataFeeds
|
|
{
|
|
/// <summary>
|
|
/// Subscription data reader is a wrapper on the stream reader class to download, unpack and iterate over a data file.
|
|
/// </summary>
|
|
/// <remarks>The class accepts any subscription configuration and automatically makes it available to enumerate</remarks>
|
|
public class SubscriptionDataReader : IEnumerator<BaseData>, ITradableDatesNotifier, IDataProviderEvents
|
|
{
|
|
private bool _initialized;
|
|
|
|
// Source string to create memory stream:
|
|
private SubscriptionDataSource _source;
|
|
|
|
private bool _endOfStream;
|
|
|
|
private IEnumerator<BaseData> _subscriptionFactoryEnumerator;
|
|
|
|
/// Configuration of the data-reader:
|
|
private readonly SubscriptionDataConfig _config;
|
|
|
|
/// true if we can find a scale factor file for the security of the form: ..\Lean\Data\equity\market\factor_files\{SYMBOL}.csv
|
|
private bool _hasScaleFactors;
|
|
|
|
// Location of the datafeed - the type of this data.
|
|
|
|
// Create a single instance to invoke all Type Methods:
|
|
private BaseData _dataFactory;
|
|
|
|
//Start finish times of the backtest:
|
|
private DateTime _periodStart;
|
|
private readonly DateTime _periodFinish;
|
|
|
|
private readonly MapFileResolver _mapFileResolver;
|
|
private readonly IFactorFileProvider _factorFileProvider;
|
|
private FactorFile _factorFile;
|
|
private MapFile _mapFile;
|
|
|
|
private bool _pastDelistedDate;
|
|
|
|
// true if we're in live mode, false otherwise
|
|
private readonly bool _isLiveMode;
|
|
|
|
private BaseData _previous;
|
|
private decimal? _lastRawPrice;
|
|
private readonly IEnumerator<DateTime> _tradeableDates;
|
|
|
|
// used when emitting aux data from within while loop
|
|
private readonly IDataCacheProvider _dataCacheProvider;
|
|
private DateTime _delistingDate;
|
|
|
|
/// <summary>
|
|
/// Event fired when an invalid configuration has been detected
|
|
/// </summary>
|
|
public event EventHandler<InvalidConfigurationDetectedEventArgs> InvalidConfigurationDetected;
|
|
|
|
/// <summary>
|
|
/// Event fired when the numerical precision in the factor file has been limited
|
|
/// </summary>
|
|
public event EventHandler<NumericalPrecisionLimitedEventArgs> NumericalPrecisionLimited;
|
|
|
|
/// <summary>
|
|
/// Event fired when the start date has been limited
|
|
/// </summary>
|
|
public event EventHandler<StartDateLimitedEventArgs> StartDateLimited;
|
|
|
|
/// <summary>
|
|
/// Event fired when there was an error downloading a remote file
|
|
/// </summary>
|
|
public event EventHandler<DownloadFailedEventArgs> DownloadFailed;
|
|
|
|
/// <summary>
|
|
/// Event fired when there was an error reading the data
|
|
/// </summary>
|
|
public event EventHandler<ReaderErrorDetectedEventArgs> ReaderErrorDetected;
|
|
|
|
/// <summary>
|
|
/// Event fired when there is a new tradable date
|
|
/// </summary>
|
|
public event EventHandler<NewTradableDateEventArgs> NewTradableDate;
|
|
|
|
/// <summary>
|
|
/// Last read BaseData object from this type and source
|
|
/// </summary>
|
|
public BaseData Current
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Explicit Interface Implementation for Current
|
|
/// </summary>
|
|
object IEnumerator.Current
|
|
{
|
|
get { return Current; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Subscription data reader takes a subscription request, loads the type, accepts the data source and enumerate on the results.
|
|
/// </summary>
|
|
/// <param name="config">Subscription configuration object</param>
|
|
/// <param name="periodStart">Start date for the data request/backtest</param>
|
|
/// <param name="periodFinish">Finish date for the data request/backtest</param>
|
|
/// <param name="mapFileResolver">Used for resolving the correct map files</param>
|
|
/// <param name="factorFileProvider">Used for getting factor files</param>
|
|
/// <param name="dataCacheProvider">Used for caching files</param>
|
|
/// <param name="tradeableDates">Defines the dates for which we'll request data, in order, in the security's data time zone</param>
|
|
/// <param name="isLiveMode">True if we're in live mode, false otherwise</param>
|
|
public SubscriptionDataReader(SubscriptionDataConfig config,
|
|
DateTime periodStart,
|
|
DateTime periodFinish,
|
|
MapFileResolver mapFileResolver,
|
|
IFactorFileProvider factorFileProvider,
|
|
IEnumerable<DateTime> tradeableDates,
|
|
bool isLiveMode,
|
|
IDataCacheProvider dataCacheProvider)
|
|
{
|
|
//Save configuration of data-subscription:
|
|
_config = config;
|
|
|
|
//Save Start and End Dates:
|
|
_periodStart = periodStart;
|
|
_periodFinish = periodFinish;
|
|
_mapFileResolver = mapFileResolver;
|
|
_factorFileProvider = factorFileProvider;
|
|
_dataCacheProvider = dataCacheProvider;
|
|
|
|
//Save access to securities
|
|
_isLiveMode = isLiveMode;
|
|
_tradeableDates = tradeableDates.GetEnumerator();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes the <see cref="SubscriptionDataReader"/> instance
|
|
/// </summary>
|
|
/// <remarks>Should be called after all consumers of <see cref="NewTradableDate"/> event are set,
|
|
/// since it will produce events.</remarks>
|
|
public void Initialize()
|
|
{
|
|
if (_initialized)
|
|
{
|
|
return;
|
|
}
|
|
|
|
//Save the type of data we'll be getting from the source.
|
|
try
|
|
{
|
|
_dataFactory = _config.GetBaseDataInstance();
|
|
}
|
|
catch (ArgumentException exception)
|
|
{
|
|
OnInvalidConfigurationDetected(new InvalidConfigurationDetectedEventArgs(_config.Symbol, exception.Message));
|
|
_endOfStream = true;
|
|
return;
|
|
}
|
|
|
|
//If its quandl set the access token in data factory:
|
|
var quandl = _dataFactory as Quandl;
|
|
if (quandl != null)
|
|
{
|
|
if (!Quandl.IsAuthCodeSet)
|
|
{
|
|
Quandl.SetAuthCode(Config.Get("quandl-auth-token"));
|
|
}
|
|
}
|
|
|
|
// If Tiingo data, set the access token in data factory
|
|
var tiingo = _dataFactory as TiingoPrice;
|
|
if (tiingo != null)
|
|
{
|
|
if (!Tiingo.IsAuthCodeSet)
|
|
{
|
|
Tiingo.SetAuthCode(Config.Get("tiingo-auth-token"));
|
|
}
|
|
}
|
|
|
|
// If USEnergyAPI data, set the access token in data factory
|
|
var energyInformation = _dataFactory as USEnergyAPI;
|
|
if (energyInformation != null)
|
|
{
|
|
if (!USEnergyAPI.IsAuthCodeSet)
|
|
{
|
|
USEnergyAPI.SetAuthCode(Config.Get("us-energy-information-auth-token"));
|
|
}
|
|
}
|
|
|
|
// If Fred data, set the access token in data factory
|
|
var fred = _dataFactory as FredApi;
|
|
if (fred != null)
|
|
{
|
|
if (!FredApi.IsAuthCodeSet)
|
|
{
|
|
FredApi.SetAuthCode(Config.Get("fred-auth-token"));
|
|
}
|
|
}
|
|
|
|
_factorFile = new FactorFile(_config.Symbol.Value, new List<FactorFileRow>());
|
|
_mapFile = new MapFile(_config.Symbol.Value, new List<MapFileRow>());
|
|
|
|
// load up the map files for equities, options, and custom data if it supports it.
|
|
// Only load up factor files for equities
|
|
if (_dataFactory.RequiresMapping())
|
|
{
|
|
try
|
|
{
|
|
var mapFile = _mapFileResolver.ResolveMapFile(_config.Symbol, _config.Type);
|
|
|
|
// only take the resolved map file if it has data, otherwise we'll use the empty one we defined above
|
|
if (mapFile.Any()) _mapFile = mapFile;
|
|
|
|
if (!_config.IsCustomData && _config.SecurityType != SecurityType.Option && _config.SecurityType != SecurityType.FutureOption)
|
|
{
|
|
var factorFile = _factorFileProvider.Get(_config.Symbol);
|
|
_hasScaleFactors = factorFile != null;
|
|
if (_hasScaleFactors)
|
|
{
|
|
_factorFile = factorFile;
|
|
|
|
// if factor file has minimum date, update start period if before minimum date
|
|
if (!_isLiveMode && _factorFile != null && _factorFile.FactorFileMinimumDate.HasValue)
|
|
{
|
|
if (_periodStart < _factorFile.FactorFileMinimumDate.Value)
|
|
{
|
|
_periodStart = _factorFile.FactorFileMinimumDate.Value;
|
|
|
|
OnNumericalPrecisionLimited(
|
|
new NumericalPrecisionLimitedEventArgs(_config.Symbol,
|
|
$"Data for symbol {_config.Symbol.Value} has been limited due to numerical precision issues in the factor file. " +
|
|
$"The starting date has been set to {_factorFile.FactorFileMinimumDate.Value.ToShortDateString()}."));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (_periodStart < mapFile.FirstDate)
|
|
{
|
|
var originalStart = _periodStart;
|
|
_periodStart = mapFile.FirstDate;
|
|
|
|
OnStartDateLimited(
|
|
new StartDateLimitedEventArgs(_config.Symbol,
|
|
$"The starting date for symbol {_config.Symbol.Value}," +
|
|
$" {originalStart.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)}, has been adjusted to match map file first date" +
|
|
$" {mapFile.FirstDate.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)}."));
|
|
}
|
|
}
|
|
}
|
|
catch (Exception err)
|
|
{
|
|
Log.Error(err, "Fetching Price/Map Factors: " + _config.Symbol.ID + ": ");
|
|
}
|
|
}
|
|
|
|
_delistingDate = _config.Symbol.GetDelistingDate(_mapFile);
|
|
|
|
// adding a day so we stop at EOD
|
|
_delistingDate = _delistingDate.AddDays(1);
|
|
|
|
UpdateDataEnumerator(true);
|
|
|
|
_initialized = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Advances the enumerator to the next element of the collection.
|
|
/// </summary>
|
|
/// <returns>
|
|
/// true if the enumerator was successfully advanced to the next element; false if the enumerator has passed the end of the collection.
|
|
/// </returns>
|
|
/// <exception cref="T:System.InvalidOperationException">The collection was modified after the enumerator was created. </exception><filterpriority>2</filterpriority>
|
|
public bool MoveNext()
|
|
{
|
|
if (!_initialized)
|
|
{
|
|
// Late initialization so it is performed in the data feed stack
|
|
// and not in the algorithm thread
|
|
Initialize();
|
|
}
|
|
|
|
if (_endOfStream)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (Current != null)
|
|
{
|
|
// only save previous price data
|
|
_previous = Current;
|
|
}
|
|
|
|
if (_subscriptionFactoryEnumerator == null)
|
|
{
|
|
// in live mode the trade able dates will eventually advance to the next
|
|
if (_isLiveMode)
|
|
{
|
|
// HACK attack -- we don't want to block in live mode
|
|
Current = null;
|
|
return true;
|
|
}
|
|
|
|
_endOfStream = true;
|
|
return false;
|
|
}
|
|
|
|
do
|
|
{
|
|
if (_pastDelistedDate)
|
|
{
|
|
break;
|
|
}
|
|
// keep enumerating until we find something that is within our time frame
|
|
while (_subscriptionFactoryEnumerator.MoveNext())
|
|
{
|
|
var instance = _subscriptionFactoryEnumerator.Current;
|
|
if (instance == null)
|
|
{
|
|
// keep reading until we get valid data
|
|
continue;
|
|
}
|
|
|
|
// prevent emitting past data, this can happen when switching symbols on daily data
|
|
if (_previous != null && _config.Resolution != Resolution.Tick)
|
|
{
|
|
if (_config.IsCustomData)
|
|
{
|
|
// Skip the point if time went backwards for custom data?
|
|
// TODO: Should this be the case for all datapoints?
|
|
if (instance.EndTime < _previous.EndTime) continue;
|
|
}
|
|
else
|
|
{
|
|
// all other resolutions don't allow duplicate end times
|
|
if (instance.EndTime <= _previous.EndTime) continue;
|
|
}
|
|
}
|
|
|
|
if (instance.EndTime < _periodStart)
|
|
{
|
|
// keep reading until we get a value on or after the start
|
|
_previous = instance;
|
|
continue;
|
|
}
|
|
|
|
if (instance.Time > _periodFinish)
|
|
{
|
|
// stop reading when we get a value after the end
|
|
_endOfStream = true;
|
|
return false;
|
|
}
|
|
|
|
// if we move past our current 'date' then we need to do daily things, such
|
|
// as updating factors and symbol mapping
|
|
if (instance.EndTime.ConvertTo(_config.ExchangeTimeZone, _config.DataTimeZone).Date > _tradeableDates.Current)
|
|
{
|
|
// this is fairly hacky and could be solved by removing the aux data from this class
|
|
// the case is with coarse data files which have many daily sized data points for the
|
|
// same date,
|
|
if (!_config.IsInternalFeed)
|
|
{
|
|
// lets keep this, it will be advanced by 'ResolveDataEnumerator'
|
|
var currentTradeableDate = _tradeableDates.Current;
|
|
|
|
if (UpdateDataEnumerator(false))
|
|
{
|
|
if (instance.Time.ConvertTo(_config.ExchangeTimeZone, _config.DataTimeZone).Date > currentTradeableDate)
|
|
{
|
|
if (_subscriptionFactoryEnumerator == null)
|
|
{
|
|
// the end
|
|
break;
|
|
}
|
|
// Skip current 'instance' if its start time is beyond the current date, fixes GH issue 3912
|
|
continue;
|
|
}
|
|
// its not beyond 'currentTradeableDate' lets use current instance
|
|
}
|
|
// if we DO NOT get a new enumerator we use current instance, means its a valid source
|
|
// even if after 'currentTradeableDate'
|
|
}
|
|
}
|
|
|
|
// we've made it past all of our filters, we're withing the requested start/end of the subscription,
|
|
// we've satisfied user and market hour filters, so this data is good to go as current
|
|
Current = instance;
|
|
|
|
// we keep the last raw price registered before we return so we are not affected by anyone (price scale) modifying our current
|
|
_lastRawPrice = Current.Price;
|
|
return true;
|
|
}
|
|
|
|
// we've ended the enumerator, time to refresh
|
|
UpdateDataEnumerator(true);
|
|
}
|
|
while (_subscriptionFactoryEnumerator != null);
|
|
|
|
_endOfStream = true;
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolves the next enumerator to be used in <see cref="MoveNext"/> and updates
|
|
/// <see cref="_subscriptionFactoryEnumerator"/>
|
|
/// </summary>
|
|
/// <returns>True, if the enumerator has been updated (even if updated to null)</returns>
|
|
private bool UpdateDataEnumerator(bool endOfEnumerator)
|
|
{
|
|
do
|
|
{
|
|
// always advance the date enumerator, this function is intended to be
|
|
// called on date changes, never return null for live mode, we'll always
|
|
// just keep trying to refresh the subscription
|
|
DateTime date;
|
|
if (!TryGetNextDate(out date) && !_isLiveMode)
|
|
{
|
|
_subscriptionFactoryEnumerator = null;
|
|
// if we run out of dates then we're finished with this subscription
|
|
return true;
|
|
}
|
|
|
|
// fetch the new source, using the data time zone for the date
|
|
var newSource = _dataFactory.GetSource(_config, date, _isLiveMode);
|
|
|
|
// check if we should create a new subscription factory
|
|
var sourceChanged = _source != newSource && newSource.Source != "";
|
|
var liveRemoteFile = _isLiveMode && (_source == null || _source.TransportMedium == SubscriptionTransportMedium.RemoteFile);
|
|
if (sourceChanged || liveRemoteFile)
|
|
{
|
|
// dispose of the current enumerator before creating a new one
|
|
Dispose();
|
|
|
|
// save off for comparison next time
|
|
_source = newSource;
|
|
var subscriptionFactory = CreateSubscriptionFactory(newSource, _dataFactory);
|
|
_subscriptionFactoryEnumerator = subscriptionFactory.Read(newSource).GetEnumerator();
|
|
return true;
|
|
}
|
|
|
|
// if there's still more in the enumerator and we received the same source from the GetSource call
|
|
// above, then just keep using the same enumerator as we were before
|
|
if (!endOfEnumerator) // && !sourceChanged is always true here
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// keep churning until we find a new source or run out of tradeable dates
|
|
// in live mode tradeable dates won't advance beyond today's date, but
|
|
// TryGetNextDate will return false if it's already at today
|
|
}
|
|
while (true);
|
|
}
|
|
|
|
private ISubscriptionDataSourceReader CreateSubscriptionFactory(SubscriptionDataSource source, BaseData baseDataInstance)
|
|
{
|
|
var factory = SubscriptionDataSourceReader.ForSource(source, _dataCacheProvider, _config, _tradeableDates.Current, _isLiveMode, baseDataInstance);
|
|
AttachEventHandlers(factory, source);
|
|
return factory;
|
|
}
|
|
|
|
private void AttachEventHandlers(ISubscriptionDataSourceReader dataSourceReader, SubscriptionDataSource source)
|
|
{
|
|
// NOTE: There seems to be some overlap in InvalidSource and CreateStreamReaderError
|
|
// this may be worthy of further investigation and potential consolidation of events.
|
|
|
|
// handle missing files
|
|
dataSourceReader.InvalidSource += (sender, args) =>
|
|
{
|
|
switch (args.Source.TransportMedium)
|
|
{
|
|
case SubscriptionTransportMedium.LocalFile:
|
|
// the local uri doesn't exist, write an error and return null so we we don't try to get data for today
|
|
// Log.Trace(string.Format("SubscriptionDataReader.GetReader(): Could not find QC Data, skipped: {0}", source));
|
|
break;
|
|
|
|
case SubscriptionTransportMedium.RemoteFile:
|
|
OnDownloadFailed(
|
|
new DownloadFailedEventArgs(_config.Symbol,
|
|
$"Error downloading custom data source file, skipped: {source} " +
|
|
$"Error: {args.Exception.Message}", args.Exception.StackTrace));
|
|
break;
|
|
|
|
case SubscriptionTransportMedium.Rest:
|
|
break;
|
|
|
|
default:
|
|
throw new ArgumentOutOfRangeException();
|
|
}
|
|
};
|
|
|
|
if (dataSourceReader is TextSubscriptionDataSourceReader)
|
|
{
|
|
// handle empty files/instantiation errors
|
|
var textSubscriptionFactory = (TextSubscriptionDataSourceReader)dataSourceReader;
|
|
textSubscriptionFactory.CreateStreamReaderError += (sender, args) =>
|
|
{
|
|
if (_config.IsCustomData && !_config.Type.GetBaseDataInstance().IsSparseData())
|
|
{
|
|
OnDownloadFailed(
|
|
new DownloadFailedEventArgs(_config.Symbol,
|
|
"We could not fetch the requested data. " +
|
|
"This may not be valid data, or a failed download of custom data. " +
|
|
$"Skipping source ({args.Source.Source})."));
|
|
}
|
|
};
|
|
|
|
// handle parser errors
|
|
textSubscriptionFactory.ReaderError += (sender, args) =>
|
|
{
|
|
OnReaderErrorDetected(
|
|
new ReaderErrorDetectedEventArgs(_config.Symbol,
|
|
$"Error invoking {_config.Symbol} data reader. " +
|
|
$"Line: {args.Line} Error: {args.Exception.Message}",
|
|
args.Exception.StackTrace));
|
|
};
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Iterates the tradeable dates enumerator
|
|
/// </summary>
|
|
/// <param name="date">The next tradeable date</param>
|
|
/// <returns>True if we got a new date from the enumerator, false if it's exhausted, or in live mode if we're already at today</returns>
|
|
private bool TryGetNextDate(out DateTime date)
|
|
{
|
|
if (_isLiveMode && _tradeableDates.Current >= DateTime.Today)
|
|
{
|
|
// special behavior for live mode, don't advance past today
|
|
date = _tradeableDates.Current;
|
|
return false;
|
|
}
|
|
|
|
while (_tradeableDates.MoveNext())
|
|
{
|
|
date = _tradeableDates.Current;
|
|
|
|
OnNewTradableDate(new NewTradableDateEventArgs(date, _previous, _config.Symbol, _lastRawPrice));
|
|
|
|
if (_pastDelistedDate || date > _delistingDate)
|
|
{
|
|
// if we already passed our delisting date we stop
|
|
_pastDelistedDate = true;
|
|
break;
|
|
}
|
|
|
|
if (!_mapFile.HasData(date))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// don't do other checks if we haven't gotten data for this date yet
|
|
if (_previous != null && _previous.EndTime.ConvertTo(_config.ExchangeTimeZone, _config.DataTimeZone) > _tradeableDates.Current)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// we've passed initial checks,now go get data for this date!
|
|
return true;
|
|
}
|
|
|
|
// no more tradeable dates, we've exhausted the enumerator
|
|
date = DateTime.MaxValue.Date;
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reset the IEnumeration
|
|
/// </summary>
|
|
/// <remarks>Not used</remarks>
|
|
public void Reset()
|
|
{
|
|
throw new NotImplementedException("Reset method not implemented. Assumes loop will only be used once.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Dispose of the Stream Reader and close out the source stream and file connections.
|
|
/// </summary>
|
|
public void Dispose()
|
|
{
|
|
_subscriptionFactoryEnumerator?.Dispose();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Event invocator for the <see cref="InvalidConfigurationDetected"/> event
|
|
/// </summary>
|
|
/// <param name="e">Event arguments for the <see cref="InvalidConfigurationDetected"/> event</param>
|
|
protected virtual void OnInvalidConfigurationDetected(InvalidConfigurationDetectedEventArgs e)
|
|
{
|
|
InvalidConfigurationDetected?.Invoke(this, e);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Event invocator for the <see cref="NumericalPrecisionLimited"/> event
|
|
/// </summary>
|
|
/// <param name="e">Event arguments for the <see cref="NumericalPrecisionLimited"/> event</param>
|
|
protected virtual void OnNumericalPrecisionLimited(NumericalPrecisionLimitedEventArgs e)
|
|
{
|
|
NumericalPrecisionLimited?.Invoke(this, e);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Event invocator for the <see cref="StartDateLimited"/> event
|
|
/// </summary>
|
|
/// <param name="e">Event arguments for the <see cref="StartDateLimited"/> event</param>
|
|
protected virtual void OnStartDateLimited(StartDateLimitedEventArgs e)
|
|
{
|
|
StartDateLimited?.Invoke(this, e);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Event invocator for the <see cref="DownloadFailed"/> event
|
|
/// </summary>
|
|
/// <param name="e">Event arguments for the <see cref="DownloadFailed"/> event</param>
|
|
protected virtual void OnDownloadFailed(DownloadFailedEventArgs e)
|
|
{
|
|
DownloadFailed?.Invoke(this, e);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Event invocator for the <see cref="ReaderErrorDetected"/> event
|
|
/// </summary>
|
|
/// <param name="e">Event arguments for the <see cref="ReaderErrorDetected"/> event</param>
|
|
protected virtual void OnReaderErrorDetected(ReaderErrorDetectedEventArgs e)
|
|
{
|
|
ReaderErrorDetected?.Invoke(this, e);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Event invocator for the <see cref="NewTradableDate"/> event
|
|
/// </summary>
|
|
/// <param name="e">Event arguments for the <see cref="NewTradableDate"/> event</param>
|
|
protected virtual void OnNewTradableDate(NewTradableDateEventArgs e)
|
|
{
|
|
NewTradableDate?.Invoke(this, e);
|
|
}
|
|
}
|
|
}
|