API documentation

Level2Entry

Subscribe to Level2 (order book) updates for an instrument

/// <summary>
        /// Subscribe to Level2 (order book) updates for an instrument
        /// </summary>
        public void SubscribeLevel2(int instrumentId)
        {
            if (this.tickUpdatesPaused)
            {
                return;
            }

            // Already subscribed to this instrument - skip to avoid duplicate subscriptions
            if (this.level2SubscribedInstrumentId.HasValue && this.level2SubscribedInstrumentId == instrumentId)
            {
                return;
            }

            // Unsubscribe from previous instrument if any
            if (this.level2SubscribedInstrumentId.HasValue)
            {
                this.UnsubscribeLevel2();
            }

            this.level2SubscribedInstrumentId = instrumentId;
            this.level2Bids.Clear();
            this.level2Asks.Clear();
            this.level2SentTimestamp = 0;

            Logger.Information("User {UserId} subscribed to Level2 for instrument {InstrumentId}", this.UserId, instrumentId);
        }

        /// <summary>
        /// Unsubscribe from Level2 updates
        /// </summary>
        public void UnsubscribeLevel2()
        {
            if (this.level2SubscribedInstrumentId.HasValue)
            {
                Logger.Information("User {UserId} unsubscribed from Level2 for instrument {InstrumentId}", this.UserId, this.level2SubscribedInstrumentId);
                this.level2SubscribedInstrumentId = null;
                this.level2Bids.Clear();
                this.level2Asks.Clear();
            }
        }

        private async Task On(Level2Occurred @event, CancellationToken cancellationToken)
        {
            var level2 = @event.Level2;

            if (this.tickUpdatesPaused || !this.HasActiveConnections)
            {
                return;
            }

            // Only process if subscribed to this instrument
            if (!this.level2SubscribedInstrumentId.HasValue || 
                this.level2SubscribedInstrumentId.Value != level2.InstrumentId)
            {
                return;
            }

            // Update internal order book state
            this.UpdateLevel2Book(level2);

            // Throttle sending to 500ms
            var currentTicks = DateTime.UtcNow.Ticks;
            if (currentTicks - this.level2SentTimestamp < Level2SendPeriod.Ticks)
            {
                return;
            }

            this.level2SentTimestamp = currentTicks;

            try
            {
                // Check for backpressure
                var currentPendingCount = Interlocked.Read(ref this.pendingMessageCount);
                if (currentPendingCount > 100)
                {
                    return;
                }

                Instrument? instrument = null;
                this.instrumentsById?.TryGetValue(level2.InstrumentId, out instrument);

                // Build and send order book snapshot
                var snapshot = new Level2BookSnapshot(
                    level2.InstrumentId,
                    this.level2Bids.Take(Level2MaxLevels).ToArray(),
                    this.level2Asks.Take(Level2MaxLevels).ToArray(),
                    DateTime.UtcNow.ToString("o"),
                    instrument?.PriceFormat);

                Interlocked.Increment(ref this.pendingMessageCount);
                await this.clientProxy.SendAsync("UpdateLevel2", snapshot, cancellationToken)
                    .ContinueWith(task =>
                    {
                        Interlocked.Decrement(ref this.pendingMessageCount);
                        if (task.IsFaulted)
                        {
                            Logger.Warning(task.Exception, "Level2 send failed for user {UserId}, instrument {InstrumentId}",
                                this.UserId, level2.InstrumentId);
                        }
                    }, TaskContinuationOptions.ExecuteSynchronously);
            }
            catch (Exception exception) when (exception.IsNotCritical())
            {
                Logger.Error(exception, "Level2 SignalR error for user {UserId}", this.UserId);
            }
        }

        private void UpdateLevel2Book(Level2 level2)
        {
            var targetList = level2.Side == Level2Side.Bid ? this.level2Bids : this.level2Asks;
            var position = level2.Position;

            switch (level2.Action)
            {
                case Level2UpdateAction.New:
                    if (position >= 0 && position <= targetList.Count && targetList.Count < Level2MaxLevels * 2)
                    {
                        targetList.Insert(position, new Level2Entry(level2.Price, level2.Size, 0));
                    }
                    break;

                case Level2UpdateAction.Change:
                    if (position >= 0 && position < targetList.Count)
                    {
                        targetList[position] = new Level2Entry(level2.Price, level2.Size, 0);
                    }
                    break;

                case Level2UpdateAction.Delete:
                    if (position >= 0 && position < targetList.Count)
                    {
                        targetList.RemoveAt(position);
                    }
                    break;

                case Level2UpdateAction.Reset:
                    this.level2Bids.Clear();
                    this.level2Asks.Clear();
                    break;
            }

            // Recalculate totals (cumulative size)
            this.RecalculateTotals(this.level2Bids);
            this.RecalculateTotals(this.level2Asks);
        }

        private void RecalculateTotals(List<Level2Entry> entries)
        {
            decimal total = 0;
            for (int i = 0; i < entries.Count; i++)
            {
                total += entries[i].Size;
                entries[i].Total = total;
            }
        }

        private async Task On(Level2SnapshotOccurred @event, CancellationToken cancellationToken)
        {
            var snapshot = @event.Snapshot;

            if (this.tickUpdatesPaused || !this.HasActiveConnections)
            {
                return;
            }

            // Only process if subscribed to this instrument
            if (!this.level2SubscribedInstrumentId.HasValue || 
                this.level2SubscribedInstrumentId.Value != snapshot.InstrumentId)
            {
                return;
            }

            // Replace the internal order book with the snapshot data
            this.level2Bids.Clear();
            this.level2Asks.Clear();

            if (snapshot.Bids != null)
            {
                decimal total = 0;
                foreach (var bid in snapshot.Bids.Take(Level2MaxLevels))
                {
                    total += bid.Size;
                    this.level2Bids.Add(new Level2Entry(bid.Price, bid.Size, total));
                }
            }

            if (snapshot.Asks != null)
            {
                decimal total = 0;
                foreach (var ask in snapshot.Asks.Take(Level2MaxLevels))
                {
                    total += ask.Size;
                    this.level2Asks.Add(new Level2Entry(ask.Price, ask.Size, total));
                }
            }

            // Throttle sending to 500ms
            var currentTicks = DateTime.UtcNow.Ticks;
            if (currentTicks - this.level2SentTimestamp < Level2SendPeriod.Ticks)
            {
                return;
            }

            this.level2SentTimestamp = currentTicks;

            await this.SendLevel2ToClientAsync(snapshot.InstrumentId, cancellationToken);
        }

        private async Task On(Level2UpdateOccurred @event, CancellationToken cancellationToken)
        {
            var update = @event.Update;

            if (this.tickUpdatesPaused || !this.HasActiveConnections)
            {
                return;
            }

            // Only process if subscribed to this instrument
            if (!this.level2SubscribedInstrumentId.HasValue || 
                this.level2SubscribedInstrumentId.Value != update.InstrumentId)
            {
                return;
            }

            // Apply incremental updates
            if (update.Entries != null)
            {
                foreach (var entry in update.Entries)
                {
                    this.UpdateLevel2Book(entry);
                }
            }

            // Throttle sending to 500ms
            var currentTicks = DateTime.UtcNow.Ticks;
            if (currentTicks - this.level2SentTimestamp < Level2SendPeriod.Ticks)
            {
                return;
            }

            this.level2SentTimestamp = currentTicks;

            await this.SendLevel2ToClientAsync(update.InstrumentId, cancellationToken);
        }

        private async Task SendLevel2ToClientAsync(int instrumentId, CancellationToken cancellationToken)
        {
            try
            {
                // Check for backpressure
                var currentPendingCount = Interlocked.Read(ref this.pendingMessageCount);
                if (currentPendingCount > 100)
                {
                    return;
                }

                Instrument? instrument = null;
                this.instrumentsById?.TryGetValue(instrumentId, out instrument);

                // Build and send order book snapshot
                var bookSnapshot = new Level2BookSnapshot(
                    instrumentId,
                    this.level2Bids.Take(Level2MaxLevels).ToArray(),
                    this.level2Asks.Take(Level2MaxLevels).ToArray(),
                    DateTime.UtcNow.ToString("o"),
                    instrument?.PriceFormat);

                Interlocked.Increment(ref this.pendingMessageCount);
                await this.clientProxy.SendAsync("UpdateLevel2", bookSnapshot, cancellationToken)
                    .ContinueWith(task =>
                    {
                        Interlocked.Decrement(ref this.pendingMessageCount);
                        if (task.IsFaulted)
                        {
                            Logger.Warning(task.Exception, "Level2 send failed for user {UserId}, instrument {InstrumentId}",
                                this.UserId, instrumentId);
                        }
                    }, TaskContinuationOptions.ExecuteSynchronously);
            }
            catch (Exception exception) when (exception.IsNotCritical())
            {
                Logger.Error(exception, "Level2 SignalR error for user {UserId}", this.UserId);
            }
        }

        #endregion

        #region Trade (Time & Sales) Subscription

        /// <summary>
        /// Subscribe to trade updates for an instrument (Time & Sales)
        /// </summary>
        public void SubscribeTrades(int instrumentId)
        {
            if (this.tickUpdatesPaused)
            {
                return;
            }

            // Already subscribed to this instrument - skip to avoid duplicate subscriptions
            if (this.tradeSubscribedInstrumentId.HasValue && this.tradeSubscribedInstrumentId == instrumentId)
            {
                return;
            }

            // Unsubscribe from previous instrument if any
            if (this.tradeSubscribedInstrumentId.HasValue)
            {
                this.UnsubscribeTrades();
            }

            this.tradeSubscribedInstrumentId = instrumentId;
            Logger.Information("User {UserId} subscribed to trades for instrument {InstrumentId}", this.UserId, instrumentId);
        }

        /// <summary>
        /// Unsubscribe from trade updates
        /// </summary>
        public void UnsubscribeTrades()
        {
            if (this.tradeSubscribedInstrumentId.HasValue)
            {
                Logger.Information("User {UserId} unsubscribed from trades for instrument {InstrumentId}", this.UserId, this.tradeSubscribedInstrumentId);
                this.tradeSubscribedInstrumentId = null;
            }
        }

        #endregion

        private Task On(LastPriceReceived @event, CancellationToken cancellationToken)
        {
            var lastPrice = @event.LastPrice;

            Instrument? instrument = null;
            this.instrumentsById?.TryGetValue(lastPrice.InstrumentId, out instrument);

            this.instrumentLastPrices[lastPrice.InstrumentId] = new LastPriceInfo(
                lastPrice.InstrumentId,
                lastPrice.Timestamp.ToString(),
                lastPrice.Bid,
                lastPrice.BidSize,
                lastPrice.Ask,
                lastPrice.AskSize,
                lastPrice.BarTimestamp.ToString(),
                lastPrice.Open,
                lastPrice.High,
                lastPrice.Low,
                lastPrice.Close,
                instrument.PriceFormat);

            return Task.CompletedTask;
        }

        protected virtual Task SendPortfolioUpdatedAsync(CancellationToken cancellationToken)
        {
            return this.portfolioManager is null
                ? Task.CompletedTask
                : this.SendPortfolioUpdatedAsync(this.portfolioManager.Portfolio, cancellationToken);
        }

        private void UpdateInstrumentFromTick(Tick tick)
        {
            if (this.instrumentsById?.TryGetValue(tick.InstrumentId, out var instrument) == true)
            {
                switch (tick.TypeId)
                {
                    case DataObjectType.Bid:
                    {
                        instrument.Bid = (Bid)tick;
                        break;
                    }
                    case DataObjectType.Ask:
                    {
                        instrument.Ask = (Ask)tick;
                        break;
                    }
                    case DataObjectType.Trade:
                    {
                        instrument.Trade = (Trade)tick;
                        break;
                    }
                }
            }
        }

        #endregion

        private void ActivateIdleTimer()
        {
            this.idleTimer.Change(this.idleTimeout, Timeout.InfiniteTimeSpan);
        }

        private void DisableIdleTimer()
        {
            this.idleTimer.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
        }

        private void PostStatusChange(StreamConnectionStatus status, ErrorInfo? errorInfo = null)
        {
            this.Post(new StreamClientStatusChanged(status, errorInfo));
        }

        public void SendClientInfo(string message)
        {
            if (string.IsNullOrEmpty(message))
            {
                return;
            }

            this.clientProxy.SendAsync("Info", message)
                .SafeFireAndForget((exception) => Logger.Error(exception, "Client proxy send failed"));
        }

        public void SendClientError(ErrorCodes code, string message)
        {
            if (string.IsNullOrEmpty(message))
            {
                return;
            }

            this.clientProxy.SendAsync("Error", (int)code, message)
                .SafeFireAndForget((exception) => Logger.Error(exception, "Client proxy send failed"));
        }

        public void SendLoadingMessage(string message)
        {
            if (string.IsNullOrEmpty(message))
            {
                return;
            }

            this.clientProxy.SendAsync("LoadingMessage", message)
                .SafeFireAndForget((exception) => Logger.Error(exception, "Client proxy send failed"));
        }

        public void LogoutUserOnFrontEnd(string message)
        {
            if (string.IsNullOrEmpty(message))
            {
                return;
            }

            this.clientProxy.SendAsync("ForceLogoutUser", message)
                .SafeFireAndForget((exception) => Logger.Error(exception, "Client proxy send failed"));
        }

        private async Task<bool> RecreateStreamClientAsync(CancellationToken cancellationToken)
        {
            if (DateTime.UtcNow - this.streamClientCreatedAt < TimeSpan.FromSeconds(10))
            {
                return false;
            }

            if (this.streamClient.Status == ProviderStatus.Connected)
            {
                try
                {
                    var wrapper = new StreamClientAsyncWrapper(this.streamClient);
                    await wrapper.DisconnectAsync(reason: "Recreating stream client instance.", cancellationToken)
                        .ConfigureAwait(false);
                }
                catch (Exception exception)
                {
                    Logger.Error(
                        exception,
                        "Stream client failed to disconnect for user {UserId}",
                        this.UserId);
                }
            }

            this.streamClient = new ObermindStreamClient();
            this.SubscribeToStreamClientEvents();
            this.streamClientCreatedAt = DateTime.UtcNow;
            this.orderCounter = OrderIdStartValue;
            Logger.Information("Stream client is recreated for user {UserId}", this.UserId);
            return true;
        }

        private async Task LaunchStreamClientAsync(string? account, CancellationToken cancellationToken)
        {
            // this.SendClientInfo("Connecting to STREAM...");

            try
            {
                var wrapper = new StreamClientAsyncWrapper(this.streamClient);
                await wrapper.ConnectAsync(this.options, account, cancellationToken);
            }
            catch (Exception exception)
            {
                this.SendClientError(ErrorCodes.StreamConnection, "Failed to connect to STREAM");
                throw;
            }
        }

        private void OnPortfolioUpdated(object sender, PersistentPortfolioEventArgs args)
        {
            if (this.portfolioUpdatesPaused)
            {
                return;
            }

            if (DateTime.UtcNow.Ticks - this.portfolioSentTimestamp < PortfolioSendPeriod.Ticks) // limit sending portfolio update
            {
                return;
            }

            this.portfolioSentTimestamp = DateTime.UtcNow.Ticks;

            this.SendPortfolioUpdatedAsync(args.Portfolio)
                .SafeFireAndForget((exception) => Logger.Error(exception, "Client proxy send failed"));
        }

        private Task SendPortfolioUpdatedAsync(PortfolioModel model, CancellationToken cancellationToken = default)
        {
            var dto = PortfolioModelMapper.Map(model);

            if (dto.Positions.Any(pos => pos.Qty == 0))
            {
                dto.Positions = dto.Positions.Where(pos => pos.Qty > 0).ToArray();
            }

            return this.clientProxy.SendAsync("PortfolioUpdated", dto, cancellationToken);
        }

        private void OnStreamHearbeat(object? sender, OnHeartbeat args)
        {
            if (args.HeartbeatInterval.HasValue && args.HeartbeatInterval.Value != 0)
            {
                this.clientProxy.SendAsync("StreamHeartbeatResponse", args.DateTime, args.HeartbeatInterval.Value)
                    .SafeFireAndForget(exception => Logger.Error(exception, "Failed to send stream heartbeat"));
            }
            else
            {
                this.clientProxy.SendAsync("StreamHeartbeatResponse", args.DateTime)
                    .SafeFireAndForget(exception => Logger.Error(exception, "Failed to send stream heartbeat"));
            }
        }

        private void OnTransactionListLoaded(object sender, TransactionListEventArgs args)
        {
            this.clientProxy.SendAsync("TransactionsLoaded", args.Transactions)
                .SafeFireAndForget((exception) => Logger.Error(exception, "Client proxy send failed"));
        }

        private void OnAccountTransactionListLoaded(object sender, AccountTransactionListEventArgs args)
        {
            this.clientProxy.SendAsync("AccountTransactionsLoaded", args.AccountTransactions)
                .SafeFireAndForget((exception) => Logger.Error(exception, "Client proxy send failed"));
        }

        private void OnPortfolioStatisticsLoaded(object sender, PortfolioStatisticsResultListEventArgs args)
        {
            this.clientProxy.SendAsync("PortfolioStatisticsLoaded", args.PortfolioStatisticsResults)
                .SafeFireAndForget((exception) => Logger.Error(exception, "Client proxy send failed"));
        }

        private void OnPortfolioOrdersListUpdated(object sender, OrderListEventArgs args)
        {
            this.clientProxy.SendAsync("PortfolioOrdersListUpdated", args.Orders)
                .SafeFireAndForget((exception) => Logger.Error(exception, "Client proxy send failed"));
        }

        private void OnPortfolioPositionsListUpdated(object sender, PositionListEventArgs args)
        {
            // Throttle position updates to avoid flooding the websocket
            // Positions update on every tick, so we need some throttling (but less than portfolio)
            if (DateTime.UtcNow.Ticks - this.positionsSentTimestamp < PositionsSendPeriod.Ticks)
            {
                return;
            }

            this.positionsSentTimestamp = DateTime.UtcNow.Ticks;

            // Only send open positions (amount != 0), filter out closed positions
            var openPositions = args.Positions.Where(pos => pos.Amount != 0).ToArray();
            var positionDtos = PositionModelMapper.MapArray(openPositions);
            this.clientProxy.SendAsync("PortfolioPositionsListUpdated", positionDtos)
                .SafeFireAndForget((exception) => Logger.Error(exception, "Client proxy send failed"));
        }

        private void SubscribeToStreamClientEvents()
        {
            this.streamClient.Error += this.OnStreamClientError;
            this.streamClient.ForcedLogout += this.OnStreamClientForcedLogout;
            this.streamClient.StatusChanged += this.OnStreamClientStatusChanged;
            this.streamClient.ExecutionReport += this.OnStreamClientExecutionReport;
            this.streamClient.ExecutionMessagesList += this.OnStreamClientExecutionMessagesList;
            this.streamClient.Tick += this.OnStreamClientTick;
            this.streamClient.Level2Snapshot += this.OnStreamClientLevel2Snapshot;
            this.streamClient.Level2Update += this.OnStreamClientLevel2Update;
            this.streamClient.AccountData += this.OnStreamClientAccountData;
            this.streamClient.TransactionList += this.OnStreamClientTransactionList;
            this.streamClient.AccountTransactionList += this.OnStreamClientAccountTransactionList;
            // this.streamClient.PortfolioPerformanceRecordList += this.OnStreamClientPortfolioPerformanceRecordList;
            this.streamClient.PortfolioStatisticsResultList += this.OnStreamClientPortfolioStatisticsResultList;
            this.streamClient.LastPrice += this.OnStreamClientLastPrice;
            this.streamClient.Heartbeat += this.OnStreamHearbeat;
        }

        private void UnsubscribeFromStreamClientEvents()
        {
            this.streamClient.Tick -= this.OnStreamClientTick;
            this.streamClient.Level2Snapshot -= this.OnStreamClientLevel2Snapshot;
            this.streamClient.Level2Update -= this.OnStreamClientLevel2Update;
            this.streamClient.Error -= this.OnStreamClientError;
            this.streamClient.StatusChanged -= this.OnStreamClientStatusChanged;
            this.streamClient.ExecutionReport -= this.OnStreamClientExecutionReport;
            this.streamClient.ExecutionMessagesList -= this.OnStreamClientExecutionMessagesList;
            this.streamClient.AccountData -= this.OnStreamClientAccountData;
            this.streamClient.TransactionList -= this.OnStreamClientTransactionList;
            this.streamClient.AccountTransactionList -= this.OnStreamClientAccountTransactionList;
            // this.streamClient.PortfolioPerformanceRecordList -= this.OnStreamClientPortfolioPerformanceRecordList;
            this.streamClient.PortfolioStatisticsResultList -= this.OnStreamClientPortfolioStatisticsResultList;
            this.streamClient.LastPrice -= this.OnStreamClientLastPrice; // missed?
            this.streamClient.Heartbeat -= this.OnStreamHearbeat;

        }

        private void CreateAndSubscribeToPortfolioManagerEvents()
        {
            this.portfolioManager = new PersistentPortfolioManager(this.framework.Framework, true);
            this.portfolioManager.PersistentPortfolio += this.OnPortfolioUpdated;
            this.portfolioManager.TransactionList += this.OnTransactionListLoaded;
            this.portfolioManager.AccountTransactionList += this.OnAccountTransactionListLoaded;
            this.portfolioManager.PortfolioStatistics += this.OnPortfolioStatisticsLoaded;
            this.portfolioManager.OrderList += this.OnPortfolioOrdersListUpdated;
            this.portfolioManager.PositionList += this.OnPortfolioPositionsListUpdated;
        }

        #region Stream client event handlers

        private void OnStreamClientError(object? sender, DataEventArgs<Exception> args)
        {
            Logger.Error(args.Data, "Stream client error for user {UserId}", this.UserId);

            if (args.Data.Message == "Unable to read beyond the end of the stream.")
                return;

            this.PostStatusChange(
                StreamConnectionStatus.Error,
                new ErrorInfo(
                    args.Data,
                    ErrorCodes.StreamConnection,
                    $"STREAM connection error: {args.Data.Message}"));
        }

        private void OnStreamClientForcedLogout(object? sender, EventArgs e)
        {
            Logger.Information("OnStreamClientForcedLogout {UserId}", this.UserId);
            this.Dispose();
            this.AdminForcedLogout?.Invoke(this, EventArgs.Empty);
        }

        private void OnStreamClientStatusChanged(object? sender, EventArgs args)
        {
            var newStatus = this.streamClient.Status switch
            {
                ProviderStatus.Connecting => StreamConnectionStatus.Connecting,
                ProviderStatus.Connected => StreamConnectionStatus.Connected,
                ProviderStatus.Disconnected => StreamConnectionStatus.Disconnected,
                _ => this.StreamStatus
            };

            this.PostStatusChange(newStatus);
        }

        private void OnStreamClientExecutionReport(object? sender, ExecutionReportEventArgs args)
        {
            this.Post(new ExecutionReportReceived(args.Report));
        }

        private void OnStreamClientExecutionMessagesList(object? sender, StreamOrderInfoListEventArgs args)
        {
            this.Post(new ExecutionMessagesListReceived(args.OrderInfoList));
        }

        private void OnStreamClientTick(object? sender, TickEventArgs args)
        {
            this.Post(new TickOccurred(args.Tick));
            
            // Also handle Level2 data if it comes through as a tick
            if (args.Tick.TypeId == DataObjectType.Level2 && args.Tick is Level2 level2)
            {
                this.Post(new Level2Occurred(level2));
            }
        }

        private void OnStreamClientLevel2Snapshot(object? sender, Level2SnapshotEventArgs args)
        {
            this.Post(new Level2SnapshotOccurred(args.Snapshot));
        }

        private void OnStreamClientLevel2Update(object? sender, Level2UpdateEventArgs args)
        {
            this.Post(new Level2UpdateOccurred(args.Update));
        }

        private void OnStreamClientAccountData(object sender, AccountDataEventArgs args)
        {
            this.Post(new AccountDataReceived(args.Data));
        }

        private void OnStreamClientTransactionList(object sender, TransactionListEventArgs args)
        {
            this.Post(new TransactionListReceived(args.Transactions));
        }

        private void OnStreamClientAccountTransactionList(object sender, AccountTransactionListEventArgs args)
        {
            this.Post(new AccountTransactionListReceived(args.AccountTransactions));
        }

        private void OnStreamClientPortfolioPerformanceRecordList(object sender, PortfolioPerformanceRecordListEventArgs args)
        {
            this.Post(new PortfolioPerformanceRecordListReceived(args.Records));
        }

        private void OnStreamClientPortfolioStatisticsResultList(object sender, PortfolioStatisticsResultListEventArgs args)
        {
            this.Post(new PortfolioStatisticsResultListReceived(args.PortfolioStatisticsResults));
        }

        private void OnStreamClientLastPrice(object? sender, LastPriceEventArgs e)
        {
            this.Post(new LastPriceReceived(e.LastPrice));
        }

        #endregion
    }

    public sealed class LastPriceInfo
    {
        public LastPriceInfo(
            int instrumentId,
            string? timeStamp,
            double? bid,
            decimal? bidSize,
            double? ask,
            decimal? askSize,
            string? barTimeStamp,
            double? open,
            double? high,
            double? low,
            double? close,
            string? priceFormat)
        {
            this.InstrumentId = instrumentId;
            this.TimeStamp = timeStamp;
            this.Bid = bid;
            this.BidSize = bidSize;
            this.Ask = ask;
            this.AskSize = askSize;
            this.BarTimeStamp = barTimeStamp;
            this.Open = open;
            this.High = high;
            this.Low = low;
            this.Close = close;
            this.PriceFormat = priceFormat;
        }

        public int InstrumentId { get; }
        public string? TimeStamp { get; }
        public double? Bid { get; }
        public decimal? BidSize { get; }
        public double? Ask { get; }
        public decimal? AskSize { get; }
        public string? BarTimeStamp {  get; }
        public double? Open { get; }
        public double? High { get; }
        public double? Low { get; }
        public double? Close { get; }
        public string? PriceFormat { get; }
    }

    /// <summary>
    /// Represents a single price level in the order book (Level2)
    /// </summary>
    public sealed class Level2Entry
    {
        public Level2Entry(double price, decimal size, decimal total)
        {
            this.Price = price;
            this.Size = size;
            this.Total = total;
        }
Alipay
Stripe
Eurex
AWS
Azure
Google Cloud
Currenex
Velocity Trade
Bitcoin
CME
Interactive Brokers
Kraken
FIX API
XTX
Bloomberg
Binance

启动您的平台。

开始使用。