|
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:
| 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 |
|
|
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. |