chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:02:50 +08:00
commit 0fc60fdcb1
5008 changed files with 910633 additions and 0 deletions
@@ -0,0 +1,40 @@
/*
* 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 QuantConnect.Data;
namespace QuantConnect.Brokerages.LevelOneOrderBook
{
/// <summary>
/// Provides data for an event that is triggered when a new <see cref="BaseData"/> is received.
/// </summary>
public sealed class BaseDataEventArgs : EventArgs
{
/// <summary>
/// Gets the <see cref="BaseData"/> data associated with the event.
/// </summary>
public BaseData BaseData { get; }
/// <summary>
/// Initializes a new instance of the <see cref="BaseDataEventArgs"/> class with the specified <see cref="BaseData"/>.
/// </summary>
/// <param name="tick">The <see cref="BaseData"/> data associated with the event.</param>
public BaseDataEventArgs(BaseData tick)
{
BaseData = tick;
}
}
}
@@ -0,0 +1,275 @@
/*
* 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 NodaTime;
using QuantConnect.Securities;
using QuantConnect.Data.Market;
namespace QuantConnect.Brokerages.LevelOneOrderBook
{
/// <summary>
/// Provides real-time tracking of Level 1 market data (top-of-book) for a specific trading symbol.
/// Updates include best bid/ask quotes and last trade executions.
/// Publishes <see cref="Tick"/> updates to a shared <see cref="IDataAggregator"/> in a thread-safe manner.
public class LevelOneMarketData
{
/// <summary>
/// Occurs when a new tick is received, such as a last trade update or a change in bid/ask values.
/// </summary>
public event EventHandler<BaseDataEventArgs> BaseDataReceived;
/// <summary>
/// Gets the symbol this service is tracking.
/// </summary>
public Symbol Symbol { get; }
/// <summary>
/// Gets the time zone associated with the symbol's exchange.
/// Used for consistent time stamping.
/// </summary>
public DateTimeZone SymbolDateTimeZone { get; }
/// <summary>
/// Gets the price of the last executed trade.
/// </summary>
public decimal LastTradePrice { get; private set; }
/// <summary>
/// Gets the size of the last executed trade.
/// </summary>
public decimal LastTradeSize { get; private set; }
/// <summary>
/// Gets the best available bid price.
/// </summary>
public decimal BestBidPrice { get; private set; }
/// <summary>
/// Gets the size of the best available bid.
/// </summary>
public decimal BestBidSize { get; private set; }
/// <summary>
/// Gets the best available ask price.
/// </summary>
public decimal BestAskPrice { get; private set; }
/// <summary>
/// Gets the size of the best available ask.
/// </summary>
public decimal BestAskSize { get; private set; }
/// <summary>
/// Gets the latest reported open interest value.
/// </summary>
public decimal OpenInterest { get; private set; }
/// <summary>
/// Gets or sets a value indicating whether quote updates with a size of zero should be ignored
/// when updating Level 1 market data.
///
/// When set to <c>true</c>, incoming bid or ask updates with a size of zero are treated
/// as missing or incomplete and will not overwrite the existing known price or size.
/// This is typically used for real-time (non-delayed) feeds where a zero size may indicate
/// a temporary data gap rather than an actionable market change.
///
/// When set to <c>false</c> (default), zero-sized updates are applied normally,
/// which is appropriate for delayed feeds or sources where a size of zero has
/// semantic meaning (e.g., clearing out a book level).
/// </summary>
public bool IgnoreZeroSizeUpdates { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="LevelOneMarketData"/> class for a given symbol.
/// </summary>
/// <param name="symbol">The trading symbol to monitor.</param>
public LevelOneMarketData(Symbol symbol)
{
Symbol = symbol;
SymbolDateTimeZone = MarketHoursDatabase.FromDataFolder().GetExchangeHours(symbol.ID.Market, symbol, symbol.SecurityType).TimeZone;
}
/// <summary>
/// Updates the best bid and ask prices and sizes.
/// Constructs and publishes a quote <see cref="Tick"/> to the <see cref="IDataAggregator"/>.
/// </summary>
/// <param name="quoteDateTimeUtc">The UTC timestamp when the quote was received.</param>
/// <param name="bidPrice">The best bid price.</param>
/// <param name="bidSize">The size available at the best bid.</param>
/// <param name="askPrice">The best ask price.</param>
/// <param name="askSize">The size available at the best ask.</param>
/// <param name="saleCondition">The sale condition string.</param>
/// <param name="exchange">The exchange identifier.</param>
public void UpdateQuote(DateTime? quoteDateTimeUtc, decimal? bidPrice, decimal? bidSize, decimal? askPrice, decimal? askSize, string saleCondition, string exchange)
{
if (!IsValidTimestamp(quoteDateTimeUtc))
{
return;
}
if (BestAskPrice == askPrice && BestAskSize == askSize && BestBidPrice == bidPrice && BestBidSize == bidSize)
{
return;
}
var isBidUpdated = TryResolvePriceSize(bidPrice, bidSize, BestBidPrice, BestBidSize, out var resolvedBidPrice, out var resolvedBidSize);
if (isBidUpdated)
{
BestBidPrice = resolvedBidPrice;
BestBidSize = resolvedBidSize;
}
var isAskUpdated = TryResolvePriceSize(askPrice, askSize, BestAskPrice, BestAskSize, out var resolvedAskPrice, out var resolvedAskSize);
if (isAskUpdated)
{
BestAskPrice = resolvedAskPrice;
BestAskSize = resolvedAskSize;
}
if (isBidUpdated || isAskUpdated)
{
var lastQuoteTick = new Tick(quoteDateTimeUtc.Value.ConvertFromUtc(SymbolDateTimeZone), Symbol, saleCondition, exchange.GetPrimaryExchange(Symbol.SecurityType), BestBidSize, BestBidPrice, BestAskSize, BestAskPrice);
BaseDataReceived?.Invoke(this, new(lastQuoteTick));
}
}
/// <summary>
/// Updates the best bid and ask prices and sizes.
/// Constructs and publishes a quote <see cref="Tick"/> to the <see cref="IDataAggregator"/>.
/// </summary>
/// <param name="quoteDateTimeUtc">The UTC timestamp when the quote was received.</param>
/// <param name="bidPrice">The best bid price.</param>
/// <param name="bidSize">The size available at the best bid.</param>
/// <param name="askPrice">The best ask price.</param>
/// <param name="askSize">The size available at the best ask.</param>
public void UpdateQuote(DateTime? quoteDateTimeUtc, decimal? bidPrice, decimal? bidSize, decimal? askPrice, decimal? askSize)
{
UpdateQuote(quoteDateTimeUtc, bidPrice, bidSize, askPrice, askSize, string.Empty, string.Empty);
}
/// <summary>
/// Updates the last trade price and size.
/// Constructs and publishes a trade <see cref="Tick"/> to the <see cref="Data.IDataAggregator"/>.
/// </summary>
/// <param name="tradeDateTimeUtc">The UTC timestamp when the trade occurred.</param>
/// <param name="lastQuantity">The quantity of the last trade.</param>
/// <param name="lastPrice">The price at which the last trade occurred.</param>
/// <param name="saleCondition">Optional sale condition string.</param>
/// <param name="exchange">Optional exchange identifier.</param>
public void UpdateLastTrade(DateTime? tradeDateTimeUtc, decimal? lastQuantity, decimal? lastPrice, string saleCondition = "", string exchange = "")
{
if (!IsValidTimestamp(tradeDateTimeUtc))
{
return;
}
if (!TryResolvePriceSize(lastPrice, lastQuantity, LastTradePrice, LastTradeSize, out var newPrice, out var newSize))
{
return;
}
LastTradePrice = newPrice;
LastTradeSize = newSize;
var lastTradeTick = new Tick(
tradeDateTimeUtc.Value.ConvertFromUtc(SymbolDateTimeZone),
Symbol,
saleCondition,
exchange.GetPrimaryExchange(Symbol.SecurityType),
LastTradeSize,
LastTradePrice);
BaseDataReceived?.Invoke(this, new(lastTradeTick));
}
/// <summary>
/// Updates the open interest value and publishes a corresponding <see cref="Tick"/>.
/// </summary>
/// <param name="openInterestDateTimeUtc">The UTC timestamp of the open interest update.</param>
/// <param name="openInterest">The reported open interest value.</param>
public void UpdateOpenInterest(DateTime? openInterestDateTimeUtc, decimal? openInterest)
{
if (!IsValidTimestamp(openInterestDateTimeUtc) || !openInterest.HasValue)
{
return;
}
var openInterestTick = new Tick(openInterestDateTimeUtc.Value.ConvertFromUtc(SymbolDateTimeZone), Symbol, openInterest.Value);
BaseDataReceived?.Invoke(this, new(openInterestTick));
}
/// <summary>
/// Attempts to resolve the effective price and size values for a Level 1 market data update,
/// using fallback values when current data is missing, zero, or invalid.
/// </summary>
/// <param name="price">The incoming price value, if available.</param>
/// <param name="size">The incoming size value associated with the price, if available.</param>
/// <param name="bestPrice">The last known valid price used as a fallback.</param>
/// <param name="bestSize">The last known valid size used as a fallback.</param>
/// <param name="newPrice">The resolved price value to be used in the update.</param>
/// <param name="newSize">The resolved size value to be used in the update.</param>
/// <returns>
/// <c>true</c> if a valid (resolved) price and size pair was determined; otherwise, <c>false</c>.
/// </returns>
private bool TryResolvePriceSize(decimal? price, decimal? size, decimal bestPrice, decimal bestSize, out decimal newPrice, out decimal newSize)
{
if (size.HasValue && (!IgnoreZeroSizeUpdates || size.Value != 0))
{
if (price.HasValue && price.Value != 0)
{
newPrice = price.Value;
newSize = size.Value;
return true;
}
else if (bestPrice != 0)
{
newPrice = bestPrice;
newSize = size.Value;
return true;
}
}
else if (price.HasValue && price.Value != 0)
{
newPrice = price.Value;
newSize = bestSize;
return true;
}
newPrice = default;
newSize = default;
return false;
}
/// <summary>
/// Determines whether the provided timestamp is valid,
/// meaning it is non-null and not equal to the default <see cref="DateTime"/> value.
/// This is typically used to detect and ignore stale or uninitialized timestamps in market data.
/// </summary>
/// <param name="timestamp">The timestamp to validate.</param>
/// <returns>
/// <c>true</c> if the timestamp is not <c>null</c> and not <c>default(DateTime)</c>; otherwise, <c>false</c>.
/// </returns>
private static bool IsValidTimestamp(DateTime? timestamp)
{
return timestamp.HasValue && timestamp.Value != default;
}
}
}
@@ -0,0 +1,273 @@
/*
* 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.Linq;
using System.Threading;
using QuantConnect.Data;
using QuantConnect.Util;
using QuantConnect.Logging;
using System.Collections.Generic;
using System.Collections.Concurrent;
namespace QuantConnect.Brokerages.LevelOneOrderBook
{
/// <summary>
/// Manages subscriptions and real-time updates for multiple <see cref="LevelOneMarketData"/> instances.
/// Facilitates routing of quote and trade data to a shared <see cref="IDataAggregator"/> in a thread-safe manner.
/// </summary>
public sealed class LevelOneServiceManager : IDisposable
{
/// <summary>
/// The shared data aggregator that receives all tick updates from subscribed symbols.
/// </summary>
private readonly IDataAggregator _dataAggregator;
/// <summary>
/// Synchronization lock used to ensure thread safety during updates.
/// </summary>
private readonly Lock _lock = new();
/// <summary>
/// Maps subscribed symbols to their corresponding <see cref="LevelOneMarketData"/> instances.
/// </summary>
private readonly ConcurrentDictionary<Symbol, LevelOneMarketData> _levelOneServiceBySymbol = new();
/// <summary>
/// Internal subscription manager used to delegate low-level subscribe/unsubscribe logic.
/// </summary>
private readonly EventBasedDataQueueHandlerSubscriptionManager _subscriptionManager;
/// <summary>
/// Gets whether there are no active subscriptions.
/// </summary>
public bool IsEmpty => _levelOneServiceBySymbol.IsEmpty;
/// <summary>
/// Gets the number of currently subscribed symbols.
/// </summary>
public int Count => _levelOneServiceBySymbol.Count;
/// <summary>
/// Initializes a new instance of the <see cref="LevelOneServiceManager"/> class.
/// </summary>
/// <param name="dataAggregator">The aggregator to which all tick data will be published.</param>
/// <param name="subscribeCallback">Delegate used to perform symbol subscription logic.</param>
/// <param name="unsubscribeCallback">Delegate used to perform symbol unsubscription logic.</param>
public LevelOneServiceManager(IDataAggregator dataAggregator, Func<IEnumerable<Symbol>, TickType, bool> subscribeCallback, Func<IEnumerable<Symbol>, TickType, bool> unsubscribeCallback)
{
_dataAggregator = dataAggregator;
_subscriptionManager = new EventBasedDataQueueHandlerSubscriptionManager()
{
SubscribeImpl = (symbols, tickType) => SubscribeCallbackWrapper(symbols, tickType, subscribeCallback),
UnsubscribeImpl = (symbols, tickType) => UnsubscribeCallbackWrapper(symbols, tickType, unsubscribeCallback)
};
}
/// <summary>
/// Subscribes to the specified symbol based on the given <see cref="SubscriptionDataConfig"/>.
/// </summary>
/// <param name="dataConfig">The subscription configuration containing symbol and type information.</param>
public void Subscribe(SubscriptionDataConfig dataConfig)
{
_subscriptionManager.Subscribe(dataConfig);
}
/// <summary>
/// Unsubscribes from the specified symbol and removes its associated service instance.
/// </summary>
/// <param name="dataConfig">The subscription configuration used for unsubscription.</param>
public void Unsubscribe(SubscriptionDataConfig dataConfig)
{
_subscriptionManager.Unsubscribe(dataConfig);
}
/// <summary>
/// Handles incoming quote data for a symbol.
/// Deduplicates updates and routes changes to the relevant <see cref="LevelOneMarketData"/> instance.
/// </summary>
/// <param name="symbol">The symbol for which quote data is received.</param>
/// <param name="quoteDateTimeUtc">The UTC timestamp of the quote.</param>
/// <param name="bidPrice">The bid price.</param>
/// <param name="bidSize">The size at the bid price.</param>
/// <param name="askPrice">The ask price.</param>
/// <param name="askSize">The size at the ask price.</param>
public void HandleQuote(Symbol symbol, DateTime? quoteDateTimeUtc, decimal? bidPrice, decimal? bidSize, decimal? askPrice, decimal? askSize)
{
if (TryGetLevelOneMarketData(symbol, out var levelOneMarketData))
{
levelOneMarketData.UpdateQuote(quoteDateTimeUtc, bidPrice, bidSize, askPrice, askSize);
}
}
/// <summary>
/// Handles incoming last trade data for a symbol and routes it to the corresponding <see cref="LevelOneMarketData"/> instance.
/// </summary>
/// <param name="symbol">The symbol for which trade data is received.</param>
/// <param name="tradeDateTimeUtc">The UTC timestamp of the trade.</param>
/// <param name="lastQuantity">The trade size.</param>
/// <param name="lastPrice">The trade price.</param>
/// <param name="saleCondition">Optional sale condition string.</param>
/// <param name="exchange">Optional exchange identifier.</param>
public void HandleLastTrade(Symbol symbol, DateTime? tradeDateTimeUtc, decimal? lastQuantity, decimal? lastPrice, string saleCondition = "", string exchange = "")
{
if (TryGetLevelOneMarketData(symbol, out var levelOneMarketData))
{
levelOneMarketData.UpdateLastTrade(tradeDateTimeUtc, lastQuantity, lastPrice, saleCondition, exchange);
}
}
/// <summary>
/// Handles open interest updates for the specified symbol.
/// If the symbol is subscribed, forwards the open interest data to the corresponding
/// <see cref="LevelOneMarketData"/> instance for publishing.
/// </summary>
/// <param name="symbol">The trading symbol associated with the open interest update.</param>
/// <param name="openInterestDateTimeUtc">The UTC timestamp when the open interest value was observed.</param>
/// <param name="openInterest">The reported open interest value.</param>
public void HandleOpenInterest(Symbol symbol, DateTime? openInterestDateTimeUtc, decimal? openInterest)
{
if (TryGetLevelOneMarketData(symbol, out var levelOneMarketData))
{
levelOneMarketData.UpdateOpenInterest(openInterestDateTimeUtc, openInterest);
}
}
/// <summary>
/// Sets the <see cref="LevelOneMarketData.IgnoreZeroSizeUpdates"/> flag for the specified symbol,
/// controlling how zero-sized quote updates are handled for that symbol's market data stream.
/// </summary>
/// <param name="symbol">The symbol whose quote update behavior should be configured.</param>
/// <param name="ignoreZeroSizeUpdates">
/// If <c>true</c>, zero-sized bid or ask updates will be ignored for the given symbol,
/// preserving existing book values. If <c>false</c>, zero sizes will be applied as valid updates.
/// </param>
/// <remarks>
/// This is typically used to differentiate between real-time and delayed data feeds, where zero-size
/// updates in real-time may indicate incomplete data, but in delayed feeds may represent actual market states.
/// </remarks>
public void SetIgnoreZeroSizeUpdates(Symbol symbol, bool ignoreZeroSizeUpdates)
{
if (TryGetLevelOneMarketData(symbol, out var levelOneMarketData))
{
levelOneMarketData.IgnoreZeroSizeUpdates = ignoreZeroSizeUpdates;
}
}
/// <summary>
/// Returns subscribed symbols
/// </summary>
/// <returns>list of <see cref="Symbol"/> currently subscribed</returns>
public IEnumerable<Symbol> GetSubscribedSymbols()
{
return _subscriptionManager.GetSubscribedSymbols();
}
/// <summary>
/// Handles BaseData updates emitted by <see cref="LevelOneMarketData"/> instances.
/// Forwards the BaseData to the shared data aggregator in a thread-safe manner.
/// </summary>
/// <param name="_">The originator of the BaseData.</param>
/// <param name="eventData">The BaseData event data.</param>
private void BaseDataReceived(object _, BaseDataEventArgs eventData)
{
lock (_lock)
{
_dataAggregator.Update(eventData.BaseData);
}
}
/// <summary>
/// Wraps the subscription delegate to attach symbol-specific handlers and track active Level 1 services.
/// </summary>
/// <param name="symbols">The symbols to subscribe.</param>
/// <param name="tickType">The tick type to subscribe for.</param>
/// <param name="subscribeCallback">The original subscription logic delegate.</param>
/// <returns>True if the subscription was successful; otherwise, false.</returns>
private bool SubscribeCallbackWrapper(IEnumerable<Symbol> symbols, TickType tickType, Func<IEnumerable<Symbol>, TickType, bool> subscribeCallback)
{
if (subscribeCallback(symbols, tickType))
{
foreach (var symbol in symbols)
{
_levelOneServiceBySymbol[symbol] = new(symbol);
_levelOneServiceBySymbol[symbol].BaseDataReceived += BaseDataReceived;
}
return true;
}
Log.Error($"{nameof(LevelOneServiceManager)}.{nameof(SubscribeCallbackWrapper)}: Failed for symbols: {string.Join(", ", symbols.Select(s => s.Value))}");
return false;
}
/// <summary>
/// Wraps the unsubscription delegate to detach symbol-specific handlers and remove Level 1 service tracking.
/// </summary>
/// <param name="symbols">The symbols to unsubscribe.</param>
/// <param name="tickType">The tick type to unsubscribe from.</param>
/// <param name="unsubscribeCallback">The original unsubscription logic delegate.</param>
/// <returns>True if the unsubscription was successful; otherwise, false.</returns>
private bool UnsubscribeCallbackWrapper(IEnumerable<Symbol> symbols, TickType tickType, Func<IEnumerable<Symbol>, TickType, bool> unsubscribeCallback)
{
if (unsubscribeCallback(symbols, tickType))
{
foreach (var symbol in symbols)
{
if (_levelOneServiceBySymbol.TryRemove(symbol, out var levelOneService))
{
levelOneService.BaseDataReceived -= BaseDataReceived;
}
}
return true;
}
Log.Error($"{nameof(LevelOneServiceManager)}.{nameof(UnsubscribeCallbackWrapper)}: Failed for symbols: {string.Join(", ", symbols.Select(s => s.Value))}");
return true;
}
/// <summary>
/// Attempts to retrieve the <see cref="LevelOneMarketData"/> instance associated with the specified symbol.
/// </summary>
/// <param name="symbol">The symbol whose market data instance is to be retrieved.</param>
/// <param name="levelOneMarketData">
/// When this method returns, contains the <see cref="LevelOneMarketData"/> instance associated with the symbol,
/// if the symbol is found; otherwise, <c>null</c>.
/// </param>
/// <returns>
/// <c>true</c> if the symbol is found and the associated market data instance is returned;
/// otherwise, <c>false</c>. Logs an error if the symbol is not found.
/// </returns>
private bool TryGetLevelOneMarketData(Symbol symbol, out LevelOneMarketData levelOneMarketData)
{
if (_levelOneServiceBySymbol.TryGetValue(symbol, out levelOneMarketData))
{
return true;
}
Log.Error($"{nameof(LevelOneServiceManager)}.{nameof(HandleLastTrade)}: Symbol {symbol} not found in {nameof(_levelOneServiceBySymbol)}. This could indicate an unexpected symbol or a missing initialization step.");
return false;
}
/// <summary>
/// Releases all resources used by the <see cref="LevelOneServiceManager"/>.
/// </summary>
public void Dispose()
{
_subscriptionManager.Dispose();
}
}
}