public async Task HandleCombinedStreamData()
        {
            var message = $"{{\"stream\":\"{_streamName}\",\"data\":{_message}}}";

            var stream = new BinanceWebSocketStream(DefaultWebSocketClientTest.CreateMockWebSocketClient(message))
            {
                Uri = _uri // NOTE: Processing of combined stream data is not dependant upon URI.
            };

            var isMessageEventReceived = false;

            using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3)))
            {
                stream.Message += (s, e) =>
                {
                    isMessageEventReceived = e.Subject == _streamName && e.Json == _message;
                };

                Assert.False(isMessageEventReceived);

                await stream.StreamAsync(cts.Token);

                Assert.True(isMessageEventReceived);
            }
        }
Ejemplo n.º 2
0
        /// <summary>
        /// Example using cache, web socket stream (or client), and controller.
        /// </summary>
        /// <returns></returns>
        // ReSharper disable once UnusedMember.Local
        private static void AdvancedExampleMain()
        {
            try
            {
                // Load configuration.
                var configuration = new ConfigurationBuilder()
                                    .SetBasePath(Directory.GetCurrentDirectory())
                                    .AddJsonFile("appsettings.json", false, false)
                                    .Build();

                // Configure services.
                var services = new ServiceCollection()
                               .AddBinance() // add default Binance services.
                               .AddLogging(builder => builder.SetMinimumLevel(LogLevel.Trace))
                               .BuildServiceProvider();

                // Configure logging.
                services.GetService <ILoggerFactory>()
                .AddFile(configuration.GetSection("Logging:File"));
                // NOTE: Using ":" requires Microsoft.Extensions.Configuration.Binder.

                // Get configuration settings.
                var symbols = configuration.GetSection("Statistics:Symbols").Get <string[]>()
                              ?? new string[] { Symbol.BTC_USDT };

                // Initialize cache.
                var cache = services.GetService <ISymbolStatisticsCache>();

                // Initialize web socket stream.
                var webSocket = services.GetService <IBinanceWebSocketStream>();

                using (var controller = new RetryTaskController(webSocket.StreamAsync))
                {
                    controller.Error += (s, e) => HandleError(e.Exception);

                    // Subscribe cache to symbols.
                    cache.Subscribe(Display, symbols);

                    // Set web socket URI using cache subscribed streams.
                    webSocket.Uri = BinanceWebSocketStream.CreateUri(cache);
                    // NOTE: This must be done after cache subscribe.

                    // Route stream messages to cache.
                    webSocket.Message += (s, e) => cache.HandleMessage(e.Subject, e.Json);

                    // Begin streaming.
                    controller.Begin();

                    Console.ReadKey(true); // wait for user input.
                }
            }
            catch (Exception e)
            {
                Console.WriteLine(e.Message);
                Console.WriteLine();
                Console.WriteLine("  ...press any key to close window.");
                Console.ReadKey(true);
            }
        }
Ejemplo n.º 3
0
        /// <summary>
        /// Example using manager without DI framework (not recommended).
        /// </summary>
        // ReSharper disable once InconsistentNaming
        // ReSharper disable once UnusedMember.Local
        private static void ExampleMainWithoutDI()
        {
            try
            {
                // Load configuration.
                var configuration = new ConfigurationBuilder()
                                    .SetBasePath(Directory.GetCurrentDirectory())
                                    .AddJsonFile("appsettings.json", true, false)
                                    .Build();

                // Get configuration settings.
                var symbols = configuration.GetSection("TradeHistory:Symbols").Get <string[]>()
                              ?? new string[] { Symbol.BTC_USDT };

                var limit = 25;
                try { limit = Convert.ToInt32(configuration.GetSection("TradeHistory")?["Limit"]); }
                catch { /* ignore */ }

                var loggerFactory = new LoggerFactory();
                loggerFactory.AddFile(configuration.GetSection("Logging:File"));

                // Initialize all the things... a DI framework could instantiate for you...
                var api         = new BinanceApi(BinanceHttpClient.Instance, logger: loggerFactory.CreateLogger <BinanceApi>());
                var tradeClient = new AggregateTradeClient(loggerFactory.CreateLogger <AggregateTradeClient>());
                var webSocket   = new DefaultWebSocketClient(logger: loggerFactory.CreateLogger <DefaultWebSocketClient>());
                var stream      = new BinanceWebSocketStream(webSocket, loggerFactory.CreateLogger <BinanceWebSocketStream>());
                var controller  = new BinanceWebSocketStreamController(api, stream, loggerFactory.CreateLogger <BinanceWebSocketStreamController>());
                var publisher   = new BinanceWebSocketStreamPublisher(controller, loggerFactory.CreateLogger <BinanceWebSocketStreamPublisher>());
                var client      = new AggregateTradeWebSocketClient(tradeClient, publisher, loggerFactory.CreateLogger <AggregateTradeWebSocketClient>());
                var cache       = new AggregateTradeWebSocketCache(api, client, loggerFactory.CreateLogger <AggregateTradeWebSocketCache>());

                // Add error event handler.
                controller.Error += (s, e) => HandleError(e.Exception);

                foreach (var symbol in symbols)
                {
                    // Subscribe to symbol with callback.
                    cache.Subscribe(symbol, limit, Display);

                    lock (_sync)
                    {
                        _message = symbol == symbols.Last()
                            ? $"Symbol: \"{symbol}\" ...press any key to exit."
                            : $"Symbol: \"{symbol}\" ...press any key to continue.";
                    }
                    Console.ReadKey(true);

                    // Unsubscribe from symbol.
                    cache.Unsubscribe();
                }
            }
            catch (Exception e)
            {
                Console.WriteLine(e.Message);
                Console.WriteLine();
                Console.WriteLine("  ...press any key to close window.");
                Console.ReadKey(true);
            }
        }
        public void IsCombined()
        {
            Assert.False(_stream.IsCombined);

            _stream.Uri = BinanceWebSocketStream.CreateUri(_streamName);

            Assert.False(_stream.IsCombined);

            _stream.Uri = BinanceWebSocketStream.CreateUri(_streamName, "combined");

            Assert.True(_stream.IsCombined);
        }
        public void CreateUri()
        {
            var uri = BinanceWebSocketStream.CreateUri();

            Assert.Null(uri);

            uri = BinanceWebSocketStream.CreateUri(_streamName);

            Assert.Equal($"{BinanceWebSocketStream.BaseUri}/ws/{_streamName}", uri.AbsoluteUri);

            // Duplicates are ignored.
            var uri2 = BinanceWebSocketStream.CreateUri(_streamName, _streamName);

            Assert.Equal(uri, uri2);

            const string streamName = "combined";

            uri = BinanceWebSocketStream.CreateUri(_streamName, streamName);

            Assert.Equal($"{BinanceWebSocketStream.BaseUri}/stream?streams={_streamName}/{streamName}", uri.AbsoluteUri);
        }
Ejemplo n.º 6
0
        /// <summary>
        /// Example using cache, web socket stream (or client), and controller.
        /// </summary>
        // ReSharper disable once UnusedMember.Local
        private static void AdvancedExampleMain()
        {
            try
            {
                // Load configuration.
                var configuration = new ConfigurationBuilder()
                                    .SetBasePath(Directory.GetCurrentDirectory())
                                    .AddJsonFile("appsettings.json", true, false)
                                    .Build();

                // Configure services.
                var services = new ServiceCollection()
                               .AddBinance() // add default Binance services.
                               .AddLogging(builder => builder.SetMinimumLevel(LogLevel.Trace))
                               .BuildServiceProvider();

                // Configure logging.
                services.GetService <ILoggerFactory>()
                .AddFile(configuration.GetSection("Logging:File"));
                // NOTE: Using ":" requires Microsoft.Extensions.Configuration.Binder.

                // Get configuration settings.
                var symbol = configuration.GetSection("PriceChart")?["Symbol"] ?? Symbol.BTC_USDT;

                var interval = CandlestickInterval.Minute;
                try { interval = configuration.GetSection("PriceChart")?["Interval"].ToCandlestickInterval() ?? CandlestickInterval.Minute; }
                catch { /* ignore */ }

                var limit = 25;
                try { limit = Convert.ToInt32(configuration.GetSection("PriceChart")?["Limit"] ?? "25"); }
                catch { /* ignore */ }

                // Initialize cache.
                var cache = services.GetService <ICandlestickCache>();

                // Initialize stream.
                var webSocket = services.GetService <IBinanceWebSocketStream>();

                // Initialize controller.
                using (var controller = new RetryTaskController(webSocket.StreamAsync))
                {
                    controller.Error += (s, e) => HandleError(e.Exception);

                    // Subscribe cache to symbol and interval with limit and callback.
                    cache.Subscribe(symbol, interval, limit, Display);

                    // Set web socket URI using cache subscribed streams.
                    webSocket.Uri = BinanceWebSocketStream.CreateUri(cache);
                    // NOTE: This must be done after cache subscribe.

                    // Route stream messages to cache.
                    webSocket.Message += (s, e) => cache.HandleMessage(e.Subject, e.Json);

                    // Begin streaming.
                    controller.Begin();

                    lock (_sync) _message = "...press any key to continue.";
                    Console.ReadKey(true); // wait for user input.
                }

                //*/////////////////////////////////////////
                // Alternative usage (with an IJsonClient).
                ////////////////////////////////////////////

                // Initialize publisher/client.
                var client = services.GetService <ICandlestickClient>();

                cache.Client = client; // link [new] client to cache.

                // Initialize controller.
                using (var controller = new RetryTaskController(webSocket.StreamAsync))
                {
                    controller.Error += (s, e) => HandleError(e.Exception);

                    // Subscribe cache to symbol and interval with limit and callback.
                    //cache.Subscribe(symbol, interval, limit, Display);
                    // NOTE: Cache is already subscribed to symbol (above).

                    // Begin streaming.
                    controller.Begin();

                    lock (_sync) _message = "(alternative usage) ...press any key to exit.";
                    Console.ReadKey(true); // wait for user input.
                }
            }
            catch (Exception e)
            {
                Console.WriteLine(e.Message);
                Console.WriteLine();
                Console.WriteLine("  ...press any key to close window.");
                Console.ReadKey(true);
            }
        }
        /// <summary>
        /// Example with single controller using combined streams.
        /// </summary>
        /// <returns></returns>
        public static async Task ExampleMain()
        {
            try
            {
                // Load configuration.
                var configuration = new ConfigurationBuilder()
                                    .SetBasePath(Directory.GetCurrentDirectory())
                                    .AddJsonFile("appsettings.json", true, false)
                                    .Build();

                // Configure services.
                var services = new ServiceCollection()
                               .AddBinance()                  // add default Binance services.
                               .AddLogging(builder => builder // configure logging.
                                           .SetMinimumLevel(LogLevel.Trace)
                                           .AddFile(configuration.GetSection("Logging:File")))
                               .BuildServiceProvider();

                Console.Clear(); // clear the display.

                var limit = 5;   // set to 0 to use diff. depth stream (instead of partial depth stream).

                var api = services.GetService <IBinanceApi>();

                // Create cache.
                var btcCache = services.GetService <IOrderBookCache>();
                var ethCache = services.GetService <IOrderBookCache>();

                // Create web socket stream.
                var webSocket = services.GetService <IBinanceWebSocketStream>();

                // Initialize controller.
                using (var controller = new RetryTaskController(webSocket.StreamAsync))
                {
                    controller.Error += (s, e) => HandleError(e.Exception);

                    // Query and display the order books.
                    var btcOrderBook = await api.GetOrderBookAsync(Symbol.BTC_USDT, limit);

                    var ethOrderBook = await api.GetOrderBookAsync(Symbol.ETH_BTC, limit);

                    Display(btcOrderBook, ethOrderBook);

                    // Monitor order book and display updates in real-time.
                    btcCache.Subscribe(Symbol.BTC_USDT, limit,
                                       evt =>
                    {
                        btcOrderBook = evt.OrderBook;
                        Display(btcOrderBook, ethOrderBook);
                    });

                    // Monitor order book and display updates in real-time.
                    ethCache.Subscribe(Symbol.ETH_BTC, limit,
                                       evt =>
                    {
                        ethOrderBook = evt.OrderBook;
                        Display(btcOrderBook, ethOrderBook);
                    });

                    // Set web socket URI using cache subscribed streams.
                    webSocket.Uri = BinanceWebSocketStream.CreateUri(
                        btcCache.SubscribedStreams.Concat(ethCache.SubscribedStreams));
                    // NOTE: This must be done after cache subscribe.

                    // Route stream messages to cache.
                    webSocket.Message += (s, e) => btcCache.HandleMessage(e.Subject, e.Json);
                    webSocket.Message += (s, e) => ethCache.HandleMessage(e.Subject, e.Json);

                    // Verify we are using a shared/combined stream (not necessary).
                    if (!webSocket.IsCombined())
                    {
                        throw new Exception("You are NOT using combined streams :(");
                    }

                    // Begin streaming.
                    controller.Begin();

                    Console.ReadKey(true);
                }
            }
            catch (Exception e)
            {
                Console.WriteLine(e.Message);
                Console.WriteLine();
                Console.WriteLine("  ...press any key to close window.");
                Console.ReadKey(true);
            }
        }
 public BinanceWebSocketStreamTest()
 {
     _uri    = BinanceWebSocketStream.CreateUri(_streamName);
     _stream = new BinanceWebSocketStream(DefaultWebSocketClientTest.CreateMockWebSocketClient(_message));
 }
Ejemplo n.º 9
0
        public static void AdvancedExampleMain()
        {
            try
            {
                // Load configuration.
                var configuration = new ConfigurationBuilder()
                                    .SetBasePath(Directory.GetCurrentDirectory())
                                    .AddJsonFile("appsettings.json", true, false)
                                    .Build();

                // Configure services.
                var services = new ServiceCollection()
                               .AddBinance() // add default Binance services.
                               .AddLogging(builder => builder.SetMinimumLevel(LogLevel.Trace))
                               .BuildServiceProvider();

                // Configure logging.
                services.GetService <ILoggerFactory>()
                .AddFile(configuration.GetSection("Logging:File"));
                // NOTE: Using ":" requires Microsoft.Extensions.Configuration.Binder.

                // Get configuration settings.
                var symbols = configuration.GetSection("CombinedStreamsExample:Symbols").Get <string[]>()
                              ?? new string[] { Symbol.BTC_USDT };

                // Initialize the client.
                var client = services.GetService <ISymbolStatisticsClient>();

                // Initialize the stream.
                var webSocket = services.GetService <IBinanceWebSocketStream>();

                // Initialize controller.
                using (var controller = new RetryTaskController(webSocket.StreamAsync))
                {
                    controller.Error += (s, e) => HandleError(e.Exception);

                    if (symbols.Length == 1)
                    {
                        // Subscribe to symbol with callback.
                        client.Subscribe(Display, symbols[0]);
                    }
                    else
                    {
                        // Alternative usage (combined streams).
                        client.StatisticsUpdate += (s, evt) => { Display(evt); };

                        // Subscribe to all symbols.
                        foreach (var symbol in symbols)
                        {
                            client.Subscribe(symbol); // using event instead of callbacks.
                        }
                    }

                    // Set stream URI using client subscribed streams.
                    webSocket.Uri = BinanceWebSocketStream.CreateUri(client);
                    // NOTE: This must be done after client subscribe.

                    // Route stream messages to client.
                    webSocket.Message += (s, e) => client.HandleMessage(e.Subject, e.Json);

                    // Begin streaming.
                    controller.Begin();

                    _message = "...press any key to continue.";
                    Console.ReadKey(true); // wait for user input.

                    //*//////////////////////////////////////////////////
                    // Example: Unsubscribe/Subscribe after streaming...
                    /////////////////////////////////////////////////////

                    // NOTE: When the URI is changed, the web socket is aborted
                    //       and a new connection is made. There's a delay
                    //       before streaming begins to allow for multiple
                    //       changes.

                    // Unsubscribe a symbol.
                    client.Unsubscribe(symbols[0]);

                    // Subscribe to the real Bitcoin :D
                    client.Subscribe(Symbol.BCH_USDT); // a.k.a. BCC.

                    // Set stream URI using client subscribed streams.
                    webSocket.Uri = BinanceWebSocketStream.CreateUri(client);
                    // NOTE: This must be done after client subscribe.

                    lock (_sync)
                    {
                        // Remove unsubscribed symbol and clear display (application specific).
                        _statistics.Remove(symbols[0]);
                        Console.Clear();
                    }

                    _message = "...press any key to exit.";
                    Console.ReadKey(true); // wait for user input.
                    ///////////////////////////////////////////////////*/
                }
            }
            catch (Exception e)
            {
                Console.WriteLine(e.Message);
                Console.WriteLine();
                Console.WriteLine("  ...press any key to close window.");
                Console.ReadKey(true);
            }
        }
Ejemplo n.º 10
0
        public static async Task AdvancedExampleMain(string[] args)
        {
            try
            {
                // Load configuration.
                var configuration = new ConfigurationBuilder()
                                    .SetBasePath(Directory.GetCurrentDirectory())
                                    .AddJsonFile("appsettings.json", true, false)
                                    .AddUserSecrets <Program>() // for access to API key and secret.
                                    .Build();

                // Get API key.
                var key = configuration["BinanceApiKey"]                 // user secrets configuration.
                          ?? configuration.GetSection("User")["ApiKey"]; // appsettings.json configuration.

                // Get API secret.
                var secret = configuration["BinanceApiSecret"]                 // user secrets configuration.
                             ?? configuration.GetSection("User")["ApiSecret"]; // appsettings.json configuration.

                // Configure services.
                var services = new ServiceCollection()
                               .AddBinance() // add default Binance services.
                               .AddLogging(builder => builder.SetMinimumLevel(LogLevel.Trace))
                               .BuildServiceProvider();

                // Configure logging.
                services.GetService <ILoggerFactory>()
                .AddFile(configuration.GetSection("Logging:File"));
                // NOTE: Using ":" requires Microsoft.Extensions.Configuration.Binder.

                Console.Clear(); // clear the display.

                // ReSharper disable once InconsistentlySynchronizedField
                _limit = 5;

                var symbol = Symbol.BTC_USDT;
                var asset  = symbol.BaseAsset;

                var api          = services.GetService <IBinanceApi>();
                var cache        = services.GetService <IOrderBookCache>();
                var webSocket    = services.GetService <IBinanceWebSocketStream>();
                var userProvider = services.GetService <IBinanceApiUserProvider>();

                using (var user = userProvider.CreateUser(key, secret))
                    using (var manager = services.GetService <IUserDataWebSocketManager>())
                    {
                        // Query and display order book and current asset balance.
                        var balance   = (await api.GetAccountInfoAsync(user)).GetBalance(asset);
                        var orderBook = await api.GetOrderBookAsync(symbol, _limit);

                        Display(orderBook, balance);

                        // Subscribe cache to symbol with callback.
                        cache.Subscribe(symbol, _limit, evt => Display(evt.OrderBook, balance));

                        // Set web socket URI using cache subscribed streams.
                        webSocket.Uri = BinanceWebSocketStream.CreateUri(cache);
                        // NOTE: This must be done after cache subscribe.

                        // Route stream messages to cache.
                        webSocket.Message += (s, e) => cache.HandleMessage(e.Subject, e.Json);

                        // Subscribe to symbol to display latest order book and asset balance.
                        await manager.SubscribeAsync <AccountUpdateEventArgs>(user,
                                                                              evt =>
                        {
                            // Update asset balance.
                            balance = evt.AccountInfo.GetBalance(asset);
                            // Display latest order book and asset balance.
                            Display(cache.OrderBook, balance);
                        });

                        using (var controller = new RetryTaskController(webSocket.StreamAsync))
                        {
                            controller.Error += (s, e) => HandleError(e.Exception);

                            // Begin streaming.
                            controller.Begin();

                            // Optionally, wait for web socket is connected (open).
                            await manager.WaitUntilWebSocketOpenAsync();

                            // Verify we are NOT using a combined streams (DEMONSTRATION ONLY).
                            if (webSocket.IsCombined() || webSocket == manager.Client.Publisher.Stream)
                            {
                                throw new Exception("You ARE using combined streams :(");
                            }

                            lock (_sync) _message = "...press any key to continue.";
                            Console.ReadKey(true); // wait for user input.
                        }
                    }
            }
            catch (Exception e)
            {
                Console.WriteLine(e.Message);
                Console.WriteLine();
                Console.WriteLine("...press any key to continue.");
                Console.ReadKey(true);
            }
        }
Ejemplo n.º 11
0
        /// <summary>
        /// Example using cache, web socket stream (or client), and controller.
        /// </summary>
        // ReSharper disable once UnusedMember.Local
        private static void AdvancedExampleMain()
        {
            try
            {
                // Load configuration.
                var configuration = new ConfigurationBuilder()
                                    .SetBasePath(Directory.GetCurrentDirectory())
                                    .AddJsonFile("appsettings.json", true, false)
                                    .Build();

                // Configure services.
                var services = new ServiceCollection()
                               .AddBinance() // add default Binance services.
                               .AddLogging(builder => builder.SetMinimumLevel(LogLevel.Trace))
                               .BuildServiceProvider();

                // Configure logging.
                services.GetService <ILoggerFactory>()
                .AddFile(configuration.GetSection("Logging:File"));
                // NOTE: Using ":" requires Microsoft.Extensions.Configuration.Binder.

                Console.Clear(); // clear the display.

                // Get configuration settings.
                var limit  = 10;
                var symbol = configuration.GetSection("OrderBook")?["Symbol"] ?? Symbol.BTC_USDT;
                try { limit = Convert.ToInt32(configuration.GetSection("OrderBook")?["Limit"]); }
                catch { /* ignore */ }

                // NOTE: Currently the Partial Book Depth Stream only supports limits of: 5, 10, or 20.
                if (limit > 10)
                {
                    limit = 20;
                }
                else if (limit > 5)
                {
                    limit = 10;
                }
                else if (limit > 0)
                {
                    limit = 5;
                }

                // Initialize cache.
                var cache = services.GetService <IOrderBookCache>();

                // Initialize web socket stream.
                var webSocket = services.GetService <IBinanceWebSocketStream>();

                // Initialize controller.
                using (var controller = new RetryTaskController(webSocket.StreamAsync))
                {
                    controller.Error += (s, e) => HandleError(e.Exception);

                    // Subscribe cache to symbol with limit and callback.
                    // NOTE: If no limit is provided (or limit = 0) then the order book is initialized with
                    //       limit = 1000 and the diff. depth stream is used to keep order book up-to-date.
                    cache.Subscribe(symbol, limit, Display);

                    // Set web socket URI using cache subscribed streams.
                    webSocket.Uri = BinanceWebSocketStream.CreateUri(cache);
                    // NOTE: This must be done after cache subscribe.

                    // Route stream messages to cache.
                    webSocket.Message += (s, e) => cache.HandleMessage(e.Subject, e.Json);

                    // Begin streaming.
                    controller.Begin();

                    lock (_sync) _message = "...press any key to continue.";
                    Console.ReadKey(true); // wait for user input.
                }

                //*/////////////////////////////////////////
                // Alternative usage (with an IJsonClient).
                ////////////////////////////////////////////

                // Initialize client.
                var client = services.GetService <IDepthClient>();

                cache.Client = client; // link [new] client to cache.

                // Initialize controller.
                using (var controller = new RetryTaskController(webSocket.StreamAsync))
                {
                    controller.Error += (s, e) => HandleError(e.Exception);

                    // Subscribe cache to symbol with limit and callback.
                    //cache.Subscribe(symbol, limit, Display);
                    // NOTE: Cache is already subscribed to symbol (above).

                    // Begin streaming.
                    controller.Begin();

                    lock (_sync) _message = "(alternative usage) ...press any key to exit.";
                    Console.ReadKey(true); // wait for user input.
                }
            }
            catch (Exception e)
            {
                Console.WriteLine(e.Message);
                Console.WriteLine();
                Console.WriteLine("  ...press any key to close window.");
                Console.ReadKey(true);
            }
        }
Ejemplo n.º 12
0
        /// <summary>
        /// Example with multiple controllers not using combined streams.
        /// </summary>
        /// <returns></returns>
        public static async Task ExampleMain()
        {
            try
            {
                // Load configuration.
                var configuration = new ConfigurationBuilder()
                                    .SetBasePath(Directory.GetCurrentDirectory())
                                    .AddJsonFile("appsettings.json", true, false)
                                    .Build();

                // Configure services.
                var services = new ServiceCollection()
                               .AddBinance() // add default Binance services.
                               .AddLogging(builder => builder.SetMinimumLevel(LogLevel.Trace))
                               .BuildServiceProvider();

                // Configure logging.
                services.GetService <ILoggerFactory>()
                .AddFile(configuration.GetSection("Logging:File"));
                // NOTE: Using ":" requires Microsoft.Extensions.Configuration.Binder.

                Console.Clear(); // clear the display.

                const int limit = 5;

                var api = services.GetService <IBinanceApi>();

                // Query and display the order books.
                var btcOrderBook = await api.GetOrderBookAsync(Symbol.BTC_USDT, limit);

                var ethOrderBook = await api.GetOrderBookAsync(Symbol.ETH_BTC, limit);

                Display(btcOrderBook, ethOrderBook);

                // Create cache.
                var btcCache = services.GetService <IOrderBookCache>();
                var ethCache = services.GetService <IOrderBookCache>();

                // Create web socket streams.
                var webSocket1 = services.GetService <IBinanceWebSocketStream>();
                var webSocket2 = services.GetService <IBinanceWebSocketStream>();
                // NOTE: IBinanceWebSocketStream must be setup as Transient with DI (default).

                // Initialize controllers.
                using (var controller1 = new RetryTaskController(webSocket1.StreamAsync))
                    using (var controller2 = new RetryTaskController(webSocket2.StreamAsync))
                    {
                        controller1.Error += (s, e) => HandleError(e.Exception);
                        controller2.Error += (s, e) => HandleError(e.Exception);

                        btcCache.Subscribe(Symbol.BTC_USDT, limit,
                                           evt =>
                        {
                            btcOrderBook = evt.OrderBook;
                            Display(btcOrderBook, ethOrderBook);
                        });

                        ethCache.Subscribe(Symbol.ETH_BTC, limit,
                                           evt =>
                        {
                            ethOrderBook = evt.OrderBook;
                            Display(btcOrderBook, ethOrderBook);
                        });

                        // Set web socket URI using cache subscribed streams.
                        webSocket1.Uri = BinanceWebSocketStream.CreateUri(btcCache);
                        webSocket2.Uri = BinanceWebSocketStream.CreateUri(ethCache);
                        // NOTE: This must be done after cache subscribe.

                        // Route stream messages to cache.
                        webSocket1.Message += (s, e) => btcCache.HandleMessage(e.Subject, e.Json);
                        webSocket2.Message += (s, e) => ethCache.HandleMessage(e.Subject, e.Json);

                        // Begin streaming.
                        controller1.Begin();
                        controller2.Begin();

                        // Verify we are NOT using a shared/combined stream (not necessary).
                        if (webSocket1.IsCombined() || webSocket2.IsCombined() || webSocket1 == webSocket2)
                        {
                            throw new Exception("You ARE using combined streams :(");
                        }

                        Console.ReadKey(true);
                    }
            }
            catch (Exception e)
            {
                Console.WriteLine(e.Message);
                Console.WriteLine();
                Console.WriteLine("  ...press any key to close window.");
                Console.ReadKey(true);
            }
        }