/* * 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 { /// /// 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 updates to a shared in a thread-safe manner. public class LevelOneMarketData { /// /// Occurs when a new tick is received, such as a last trade update or a change in bid/ask values. /// public event EventHandler BaseDataReceived; /// /// Gets the symbol this service is tracking. /// public Symbol Symbol { get; } /// /// Gets the time zone associated with the symbol's exchange. /// Used for consistent time stamping. /// public DateTimeZone SymbolDateTimeZone { get; } /// /// Gets the price of the last executed trade. /// public decimal LastTradePrice { get; private set; } /// /// Gets the size of the last executed trade. /// public decimal LastTradeSize { get; private set; } /// /// Gets the best available bid price. /// public decimal BestBidPrice { get; private set; } /// /// Gets the size of the best available bid. /// public decimal BestBidSize { get; private set; } /// /// Gets the best available ask price. /// public decimal BestAskPrice { get; private set; } /// /// Gets the size of the best available ask. /// public decimal BestAskSize { get; private set; } /// /// Gets the latest reported open interest value. /// public decimal OpenInterest { get; private set; } /// /// 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 true, 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 false (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). /// public bool IgnoreZeroSizeUpdates { get; set; } /// /// Initializes a new instance of the class for a given symbol. /// /// The trading symbol to monitor. public LevelOneMarketData(Symbol symbol) { Symbol = symbol; SymbolDateTimeZone = MarketHoursDatabase.FromDataFolder().GetExchangeHours(symbol.ID.Market, symbol, symbol.SecurityType).TimeZone; } /// /// Updates the best bid and ask prices and sizes. /// Constructs and publishes a quote to the . /// /// The UTC timestamp when the quote was received. /// The best bid price. /// The size available at the best bid. /// The best ask price. /// The size available at the best ask. public void UpdateQuote(DateTime? quoteDateTimeUtc, decimal? bidPrice, decimal? bidSize, decimal? askPrice, decimal? askSize) { 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, BestBidSize, BestBidPrice, BestAskSize, BestAskPrice); BaseDataReceived?.Invoke(this, new(lastQuoteTick)); } } /// /// Updates the last trade price and size. /// Constructs and publishes a trade to the . /// /// The UTC timestamp when the trade occurred. /// The quantity of the last trade. /// The price at which the last trade occurred. /// Optional sale condition string. /// Optional exchange identifier. 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, LastTradeSize, LastTradePrice); BaseDataReceived?.Invoke(this, new(lastTradeTick)); } /// /// Updates the open interest value and publishes a corresponding . /// /// The UTC timestamp of the open interest update. /// The reported open interest value. 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)); } /// /// 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. /// /// The incoming price value, if available. /// The incoming size value associated with the price, if available. /// The last known valid price used as a fallback. /// The last known valid size used as a fallback. /// The resolved price value to be used in the update. /// The resolved size value to be used in the update. /// /// true if a valid (resolved) price and size pair was determined; otherwise, false. /// 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; } /// /// Determines whether the provided timestamp is valid, /// meaning it is non-null and not equal to the default value. /// This is typically used to detect and ignore stale or uninitialized timestamps in market data. /// /// The timestamp to validate. /// /// true if the timestamp is not null and not default(DateTime); otherwise, false. /// private static bool IsValidTimestamp(DateTime? timestamp) { return timestamp.HasValue && timestamp.Value != default; } } }