Silverlight 3 Polling Duplex Chat and Realtime Stock Updates

An extension of the "End to End duplex sample" to show how Duplex polling can use the "push" model to send subscribed realtime stock price updates to connected clients.

 

I was asked to prototype something with the Duplex Polling feature of Silverlight 3 for some new Silverlight applications we're working on for eggheadcafe.com. So, like any good developer, the first thing I did was set out not to have to reinvent the wheel. I found several good examples of duplex on the web, but the one that best suited my needs was the "Duplex End to End Sample". This is a well-written simple chat service that also sends out a fake random stock price update using the "Push to all clients" model.

The stock update was interesting; I figured I could learn more about Silverlight duplex while at the same time turning this into something more realistic that could be tested with multiple clients to measure performance issues. So what I came up with was the idea that you could send a chat message, "subscribe MSFT" or "subscribe IBM". The service would then store this in such a way that it would be able to retrieve realtime stock quotes, and then using the push model, it would send out the updates for each stock to every client who had "subscribed" to updates for that stock.


To start out, I knew that I'd need to find a reliable way to get realtime stock price updates. Just about every search engine provider  now offers realtime stock quotes, but of all of them, Yahoo Finance may be the best, since they offer a simple REST-like API with a lot of features, and I've never seen them send out a response like "It appears that your request is from an automated script, so, sorry, Pal."  I caution the reader that I"ve done this for experimental purposes only, if you plan to use something like what appears here for public redisplay of stock quote information, you will want to carefully read the fine print about how and under what conditions Yahoo will allow you to do this.

The base Url for a Yahoo Finance realtime stock quote looks like this:

http://finance.yahoo.com/d/quotes.csv?s= a LIST of STOCK SYMBOLS separated by "+" &f=a concatenated list of special tags

A listing of the special tags:

So, for example:
http://finance.yahoo.com/d/quotes.csv?s=GE&f=nkqwxyr1l9t5p4
returns this:
"GENERAL ELEC CO",32.98,"Jun 26","21.30 - 32.98","NYSE",2.66,"Jul 25",28.55,"Jul 3","-0.21%" 


To get started I knew that I would need a Quote class to hold the returned data, and some sort of Utility class to handle the WebRequest and massage the data into a usable form:

Quote Class:

using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace ChatWebApp
{
    public class Quote
    {
         public string Ticker { get; set; }
        public string LastTrade { get; set; }
        public string Time { get; set;}
        public string Open { get; set; }
        public string High { get; set; }
        public string Low { get; set; }
        public string Change { get; set; }

        public Quote(){}

        public Quote( string ticker, string lastTrade, string time, string open, string high, string low, string change)
        {
             this.Ticker = ticker;
             this.LastTrade = lastTrade;
             this.Time = time;
             this.Open = open;
             this.High = high;
             this.Low = low;
             this.Change = change;
        }
    }

Note above that I'm declaring all properties as type string since this is only for display. To do computations or charting, you might want to use int, Decimal and DateTime for your properties.

QuoteUtility Class:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Web;

namespace ChatWebApp
{
    public static class QuoteUtility
    {
      
        public static string quoteUrl1 = "http://download.finance.yahoo.com/d/quotes.csv?s=";
        // symbol-last trade-last trade time -Open-High-Low-Change (real time)
        public static string quoteUrl2 = "&f=sl1t1ohgc6";

        public static List<Quote> GetQuotes (string[] tickers)
        {
            List<Quote> quotes = new List<Quote>();
             string fullQuoteUrl = quoteUrl1;
             string symbolsString = String.Empty;
             foreach(string q in tickers)
            {
                fullQuoteUrl += q + "+";
            }
            // remove the "+" sign from the end
            fullQuoteUrl = fullQuoteUrl.TrimEnd(new char[] {'+'});
            fullQuoteUrl += quoteUrl2;
            WebClient wc = new WebClient();
             string rawData = wc.DownloadString(fullQuoteUrl);
             // clear out quote marks - don't want
            rawData = rawData.Replace("\"", "");
            wc.Dispose();
            string[] quoteLines = rawData.Split(new char[] {'\r', '\n'});
            foreach(string ql in quoteLines )
             {
                 if (ql != String.Empty)
                 {
                      string[] rawQuote = ql.Split(',');
                    Quote quote = new Quote(rawQuote[0], rawQuote[1], rawQuote[2], rawQuote[3], rawQuote[4], rawQuote[5],
                                               rawQuote[6]);
                      quotes.Add(quote);
                 }
             }
             return quotes;
        }
     }

Note that the GetQuotes method accepts a string array of multiple symbols. In this example, I only get one quote at a time to keep it simple.

Now in the actual ChatService class, I have added the following:

// MasterQuotes is a Dictionary of stock symbols and a list of client sessionIds that are subscribed to that stock
private Dictionary<string, List<string>> MasterQuotes = new Dictionary<string, List<String>>();

This is used to hold the stock symbol and a List of the sessionIds of each connected client that has subscribed to the stock.

The logic for handling this is implemented as follows:

protected override void OnMessage(string sessionId, DuplexMessage data)
         {
             if (data is JoinChatMessage)
             {
                 //If a chatter joined, let all other chatters know
                JoinChatMessage msg = (JoinChatMessage)data;
                chatters.Add(sessionId, msg.nickname);
                 PushToAllClients(data);
             }
             else if (data is TextChatMessageToServer)
             {
                 //If a chatter sent a message, broadcast it to all other chatters
                TextChatMessageToServer msg = (TextChatMessageToServer)data;
                 // Check for a stock subscription---
                 if (msg.text.ToLower().Contains("subscribe"))
                 {
                     string symbol = msg.text.Split(' ')[1];
                    symbol = symbol.ToUpper();
                     // is the symbol already there? If not, add it:
                     if(!MasterQuotes.ContainsKey( symbol))
                    {
                        MasterQuotes.Add(symbol, new List<string> {sessionId });
                     }
                      else
                     {
                          //sessionIds is List<String> containing the sessionIds subscribed to this stock symbol
                        var sessionIds = MasterQuotes[symbol];
                          sessionIds.Add(sessionId);
                      }
                 }

                 else
                {
                    TextChatMessageFromServer outMsg = new TextChatMessageFromServer();
                    outMsg.text = msg.text;
                    outMsg.textColor = msg.textColor;
                     //Incoming chat message does not have the chatter's nickname, so we add it
                    outMsg.nickname = chatters[sessionId];
                     PushToAllClients(outMsg);
                 }
             }
        }

So for every message that is sent to the server by a connected client, if it contains "subscribe", we parse out the stock symbol. Then we check to see if the symbol has already been stored, and if so, we add the sessionId of the client to the List. If not, we add the symbol to the Dictionary first, and a new List<string> containing the sessionId.

Finally, the Timer fires the StockUpdate method every 30 seconds:

void StockUpdate(object o)
        {
             // iterate all the symbols we have stored for clients
            foreach (string symbol in MasterQuotes.Keys)
            {
                StockTickerMessage stm = new StockTickerMessage();
                 // get the list of sessionIds that are subscribed to this stock symbol...
                var clients = MasterQuotes[symbol];
                stm.stock = symbol;
                 // get the quote for this stock (can also do multiple symbols but keeping it simple for now)
                var quotes = QuoteUtility.GetQuotes(new string[] {stm.stock});
                stm.price = Decimal.Parse(quotes[0].LastTrade);
                stm.LastTradeTime = quotes[0].Time;
                stm.Change = quotes[0].Change;
                stm.High = quotes[0].High;
                stm.Open = quotes[0].Open;
                stm.Low = quotes[0].Low;
                 // send out the stock update message to all the clients subscribed to this symbol
                PushToSelectedClients(stm, clients);
             }
        }
PushToSelectedClients:
protected void PushToSelectedClients(DuplexMessage message,List<string> sessions)
        {
             lock (syncRoot)
             {
                 // send stock symbol update to every client who is subscribed to this stock ticker...
                 foreach (string session in sessions)
                {
                    PushMessageToClient(session, message);
                 }
             }
         }
A sample session:



You can download the Visual Studio 2008 Silverlight 3 Solution here. I hope this gives you some good ideas on using Duplex.
a Ask a2 Average Daily Volume a5 Ask Size
b Bid b2 Ask (Real-time) b3 Bid (Real-time)
b4 Book Value b6 Bid Size c Change & Percent Change
c1 Change c3 Commission c6 Change (Real-time)
c8 After Hours Change (Real-time) d Dividend/Share d1 Last Trade Date
d2 Trade Date e Earnings/Share e1 Error Indication (returned for symbol changed / invalid)
e7 EPS Estimate Current Year e8 EPS Estimate Next Year e9 EPS Estimate Next Quarter
f6 Float Shares g Day's Low h Day's High
j 52-week Low k 52-week High g1 Holdings Gain Percent
g3 Annualized Gain g4 Holdings Gain g5 Holdings Gain Percent (Real-time)
g6 Holdings Gain (Real-time) i More Info i5 Order Book (Real-time)
j1 Market Capitalization j3 Market Cap (Real-time) j4 EBITDA
j5 Change From 52-week Low j6 Percent Change From 52-week Low k1 Last Trade (Real-time) With Time
k2 Change Percent (Real-time) k3 Last Trade Size k4 Change From 52-week High
k5 Percent Change From 52-week High l Last Trade (With Time) l1 Last Trade (Price Only)
l2 High Limit l3 Low Limit m Day's Range
m2 Day's Range (Real-time) m3 50-day Moving Average m4 200-day Moving Average
m5 Change From 200-day Moving Average m6 Percent Change From 200-day Moving Average m7 Change From 50-day Moving Average
m8 Percent Change From 50-day Moving Average n Name n4 Notes
o Open p Previous Close p1 Price Paid
p2 Change in Percent p5 Price/Sales p6 Price/Book
q Ex-Dividend Date r P/E Ratio r1 Dividend Pay Date
r2 P/E Ratio (Real-time) r5 PEG Ratio r6 Price/EPS Estimate Current Year
r7 Price/EPS Estimate Next Year s Symbol s1 Shares Owned
s7 Short Ratio t1 Last Trade Time t6 Trade Links
t7 Ticker Trend t8 1 yr Target Price v Volume
v1 Holdings Value v7 Holdings Value (Real-time) w 52-week Range
w1 Day's Value Change w4 Day's Value Change (Real-time) x Stock Exchange
y Dividend Yield  
By Peter Bromberg   Popularity  (10336 Views)