使用 Silverlight 可视化实时和历史股票数据






4.89/5 (12投票s)
本文旨在提出一种在 Silverlight 中获取和可视化实时及历史股票数据的通用方法。
引言
本文旨在提出一种在 Silverlight 中获取和可视化实时及历史股票数据的通用方法。本示例使用 Google Finance 作为实时和历史股票数据源,并使用 Visiblox Charts for Silverlight 来展示获取的数据。然而,本示例的设计使得更换数据源或数据可视化库都相对容易实现。本文的结构如下:
获取实时和历史股票数据
要解决的第一个问题是找到一个提供实时和历史股票数据的数据源,并理解其 API 的工作原理。有两个数据源提供免费可访问的股票信息,但有合理的限制:Google Finance 和 Yahoo Finance。这两个数据源都提供延迟的实时股票数据信息以及历史数据。在本文中,我选择使用 Google Finance。让我们看看它的 API 是如何工作的。
Google Finance API 概述
在我们的应用程序中,我们希望能够查询股票的实时价格并获取历史价格(如前所述,Google Finance 像所有免费数据源一样提供有延迟的“实时”数据)。唯一文档化的公共 Google Finance API 是 portfolio API,但该 API 无法轻松获取股票价格。获取实时和历史股票数据的 APIunfortunately 是未文档化的。
然而,Google Finance 页面通过一个未文档化的 REST API 进行查询。通过回溯网站上的请求,可以轻松理解此 API。我们的应用程序将需要的 public
API 调用如下。
实时股票数据 API
要获取实时股票数据,需要进行以下调用:http://www.google.com/finance/info?q={stockCode}。q
参数指定股票代码,就像在 Google Finance 上指定的那样。
因此,例如,可以通过此调用查询最新的微软 (MSFT) 股票:http://www.google.com/finance/info?q=MSFT。返回的数据是 JSON 格式,上述调用将返回类似以下内容:
// [ { "id": "358464" ,"t" : "MSFT" ,"e" : "NASDAQ" ,"l" : "27.02" ,"l_cur" :
// "27.02" ,"ltt":"4:00PM EST" ,"lt" : "Dec 3, 4:00PM EST" ,"c" : "+0.13" ,"cp" :
// "0.48" ,"ccol" : "chg" ,"el": "27.02" ,"el_cur": "27.02" ,"elt" :
// "Dec 3, 7:24PM EST" ,"ec" : "0.00" ,"ecp" : "0.00" ,"eccol" : "chb" ,"div" :
// "0.16" ,"yld" : "2.37" } ]
历史股票数据 API
public
历史股票数据 API 也未文档化,因此我不得不回溯在 Google Finance 页面上进行的调用以发现它。根据此,可以使用以下调用请求历史股票数据:http://finance.google.co.uk/finance/historical?q={stockCode}&startdate={startDate}&enddate={endDate}&output=csv。
调用的参数如下:
stockCode
- 股票代码,如在 Google Finance 上指定startDate
- 期间的开始日期,格式为 MMM+d,+yyyy(例如 Jan+01,+2010)endDate
- 期间的结束日期,格式相同
一个示例查询,获取 2010 年 1 月 28 日至 2010 年 2 月 1 日之间的微软 (MSFT) 股票价格,如下所示:finance.google.co.uk/finance/historical?q=MSFT&startdate=Jan+28,+2010&enddate=Feb+01,+2010&output=csv。此调用的输出是一个简单的逗号分隔值 (CSV) 文件,结构类似:
Date, Open, High, Low, Close, Volume
1-Feb-10,28.39,28.48,27.92,28.41,85931099
29-Jan-10,29.90,29.92,27.66,28.18,193888424
28-Jan-10,29.84,29.87,28.89,29.16,117513692
解决 Silverlight 跨域问题
在映射了 Google Finance 的 REST API 后,显然我们在解决方案中将查询这些 URL。然而,为了让 Silverlight 访问跨域资源,目标域需要通过定义 crossdomain.xml 或 clientaccesspolicy.xml 并指定必需的设置来明确允许访问。由于我们想要查询的域没有此类权限,Silverlight 客户端无法直接查询此数据源。
此问题有一些解决方案,所有这些解决方案都涉及一定程度的请求代理。一种可能的方法是使用 Yahoo pipes,另一种解决方案是设置一个代理,例如使用 Google App Engine,它可以转发请求到目标域并从目标域返回。在我的解决方案中,我选择实现后者,因为代理提供了一种通用的跨域问题解决方案,而 Yahoo Pipes 需要为每种情况重新配置。
我不会深入探讨如何使用 Google App Engine 设置代理,有关详细信息,请参阅博客文章关于将 Google App Engine 用作 Silverlight 和 Flash 跨域请求代理。使用 Google App Engine 作为代理的好处是,在达到合理的每日限额之前不会收取资源费用,并且您只需要一个 Google 帐户即可开始使用。
为了让示例正常工作,我已将代理实现上传到一个自定义 app engine 域(您将在源代码中找到它)。要提交代理请求以访问微软 (MSFT) 的实时股票价格,需要进行以下请求:http://{customdomain}.appspot.com/proxy?url=http://www.google.com/finance/info?q=MSFT。
关于配额的说明
使用 Google App Engine 作为代理纯粹是出于演示目的。请注意,如果代理收到过多请求并且免费配额用完,它将停止工作。
获取和解析股票数据
在发现数据源 API 后,下一步是获取和解析一些股票数据。为此,让我们设计应用程序的数据结构并实现数据的查询和解析。
设计数据结构
我们需要为我们的应用程序表示两种不同的数据结构:实时数据和历史数据。
实时数据
Google Finance 数据源返回大约 10 个实时数据参数,我们只建模其中最重要的 4 个:股票代码、时间戳、价格和自上次更新以来的变化,从而创建 LiveStockData
类。
public class LiveStockData
{
public string StockCode { get; set; }
public DateTime TimeStamp { get; set; }
public double Price { get; set; }
public double Change { get; set; }
}
历史数据
Google Finance 数据源返回数据的高、低、开、收盘价和交易量。让我们将股票代码添加到这些信息中,并创建我们的类 - HLOCHistoricStockData
- 来表示历史 HLOC 数据。
public class HLOCHistoricStockData
{
public string StockCode { get; set; }
public DateTime Date { get; set; }
public double Price { get; set; }
public double Open { get; set; }
public double Close { get; set; }
public double High { get; set; }
public double Low { get; set; }
public double Volume { get; set; }
}
通用变更事件
由于应用程序将获取 URL,这是一个异步过程,获得这些变更通知的最简单方法是通过订阅事件。在我们的情况下,逻辑上是返回获取的 LiveStockData
或 HLOCHistoricStockData
数据结构。为了以类型安全的方式做到这一点,让我们设计一个通用的更改处理程序事件,并带有通用参数。
public delegate void StockDataEventHandler<T>(object sender, EventWithDataArgs<T> e);
public class EventWithDataArgs<T>:EventArgs
{
public T Data { get; internal set; }
public EventWithDataArgs(T data)
{
this.Data = data;
}
}
我们稍后将使用 StockDataEventHandler
作为通用事件处理程序进行更改通知。
获取和解析实时股票数据
设计完数据结构后,是时候继续获取实时数据了。思路是创建一个类,一旦创建就可以订阅它。该类将不断查询数据源,并在订阅的股票价格发生变化时触发事件。
让我们勾勒出这样一个类的接口是什么样子,并称其为 ILiveStockDataQuerier
。
public interface ILiveStockDataQuerier
{
/// <summary>
/// Event fired when the subscribed stock price has changed
/// </summary>
event StockDataEventHandler<livestockdata> OnDataUpdated;
/// <summary>
/// Subscribes to the stock, constantly queries it and
/// fires updates when it has updated
/// </summary>
void Subscribe(string stockCode);
}
继续,让我们实现这个接口。由于我们将需要不断查询,这需要在后台线程上完成,使用 BackgroundWorker
以避免应用程序挂起。当有人订阅股票代码时,我们还想存储该信息,以便知道要继续查询哪只股票。
private string _subscribedStockCode;
private BackgroundWorker _queryService;
public void Subscribe(string stockCode)
{
_subscribedStockCode = stockCode;
_queryService = new BackgroundWorker();
_queryService.DoWork += new DoWorkEventHandler((s, e) =>
{ this.QueryData(_subscribedStockCode); });
_queryService.RunWorkerAsync();
}
private void QueryData(string stockCode)
{
WebClient queryWebClient = new WebClient();
queryWebClient.OpenReadCompleted +=
new OpenReadCompletedEventHandler(liveWebClient_OpenReadCompleted);
var url = String.Format(GoogleLiveStockDataUrl
, stockCode);
var proxiedUrl = Helpers.GetProxiedUrl(url);
queryWebClient.BaseAddress = proxiedUrl;
queryWebClient.OpenReadAsync(new Uri(proxiedUrl, UriKind.Absolute));
}
一旦返回 URL,我们就需要解析它并触发带有实时股票信息的更改事件。我使用了流行的 JSON.NET 库来帮助解析 JSON 响应。代码如下:
void liveWebClient_OpenReadCompleted(object sender, OpenReadCompletedEventArgs e)
{
if (e.Error != null)
return;
var webClient = sender as WebClient;
if (webClient == null)
return;
using (StreamReader reader = new StreamReader(e.Result))
{
string contents = reader.ReadToEnd();
contents = contents.Replace("//", "");
// Change the thread culture to en-US to make parsing
// of the dates returned easier
var originalCulture = Thread.CurrentThread.CurrentCulture;
Thread.CurrentThread.CurrentCulture = new CultureInfo("en-US");
try
{
var array = JArray.Parse(contents);
if (array.Count > 0)
{
var o = array[0];
double value = o["l"].Value<double>();
double change = o["c"].Value<double>();
DateTime date = DateTime.ParseExact(o["lt"].Value<string>(),
"MMM d, h:mmtt EST", CultureInfo.CurrentUICulture);
var data = new LiveStockData()
{
StockCode = _subscribedStockCode,
TimeStamp = date,
Price = value,
Change = change
};
// For simplicity, fire a change event every time
if (OnDataUpdated != null)
{
OnDataUpdated(this,
new EventWithDataArgs<livestockdata>(data));
}
// Start over
Thread.Sleep(1000);
this.QueryData(_subscribedStockCode);
}
}
// The response could not be parsed
catch (JsonReaderException ex)
{
// Implement custom exception handling if needed
}
}
}
获取和解析历史股票数据
在实现了处理实时更新查询和解析的类之后,让我们实现获取和解析历史股票价格的类。我们期望这个类执行以下操作:可以指示它检索给定时间范围内给定股票的数据,将响应解析为 HLOCHistoricStockData
类并引发一个传递该类的事件。基于这些要求,让我们勾勒出该类的接口 - IHistoricalStockDataQuerier
。
public interface IHistoricalStockDataQuerier
{
/// <summary>
/// Event fired when the subscribed stock's data has been returned
/// </summary>
event StockDataEventHandler<IList<hlochistoricstockdata> >OnQueryCompleted;
/// <summary>
/// Queries historical data between the given dates and fires
/// OnChanged when finished.
/// </summary>
void QueryData(string stockCode, DateTime startDate, DateTime endDate);
}
实现股票数据查询非常简单。
public void QueryData(string stockCode, DateTime startDate, DateTime endDate)
{
WebClient historicWebClient = new WebClient();
historicWebClient.OpenReadCompleted +=
new OpenReadCompletedEventHandler(historicWebClient_OpenReadCompleted);
Thread.CurrentThread.CurrentCulture = new CultureInfo("en-US");
var url = String.Format(GoogleHistoricalStockDataUrl,
stockCode,
startDate.ToString("MMM+dd\\%2C+yyyy"),
endDate.ToString("MMM+dd\\%2C+yyyy")
);
var proxiedUrl = Helpers.GetProxiedUrl(url);
historicWebClient.BaseAddress = proxiedUrl;
historicWebClient.OpenReadAsync(new Uri(proxiedUrl, UriKind.Absolute));
}
解析结果也并不棘手,因为我们收到的是简单的 CSV。
void historicWebClient_OpenReadCompleted(object sender, OpenReadCompletedEventArgs e)
{
if (e.Error != null)
return;
var webClient = sender as WebClient;
if (webClient == null)
return;
try
{
using (StreamReader reader = new StreamReader(e.Result))
{
var stockDataList = new List<hlochistoricstockdata>();
string contents = reader.ReadToEnd();
var lines = contents.Split('\n');
bool firstLine = true;
foreach (var line in lines)
{
// First line is metadata ==> ignore it
if (firstLine)
{
firstLine = false;
continue;
}
// Skip last empty line
if (line.Length == 0)
continue;
var parts = line.Split(',');
// Google data is separated as Date, Open, High, Low, Close, Volume
var stockData = new HLOCHistoricStockData()
{
Date = Convert.ToDateTime(parts[0]),
Open = Convert.ToDouble(parts[1]),
High = Convert.ToDouble(parts[2]),
Low = Convert.ToDouble(parts[3]),
Close = Convert.ToDouble(parts[4]),
Price = Convert.ToDouble(parts[4]),
Volume = Convert.ToDouble(parts[5]),
StockCode = GetStockCodeFromUrl(webClient.BaseAddress)
};
stockDataList.Add(stockData);
}
// Raise the OnChanged event
if (OnQueryCompleted != null)
{
OnQueryCompleted(this, new EventWithDataArgs
<IList<hlochistoricstockdata>>(stockDataList));
}
}
}
// Something went wrong!
catch (Exception ex)
{
// Implement custom exception handling if needed
}
}
</hlochistoricstockdata>
此代码的有一部分需要解释。在实时数据查询示例中,我们在 _subscribedStockCode private
变量中保留了查询股票的代码。在历史版本中,我们没有这样做,而是在每次请求的结果中,我们可以通过从原始请求 URL 解析来访问它,调用 GetStockCodeFromUrl
。这是一个小细节,但是这样我们不仅不必存储我们正在查询的股票,而且还可以同时为多只股票运行多个查询,因为查询返回时会包含它所属股票的代码。GetStockCodeFromUrl
方法的实现如下:
private string GetStockCodeFromUrl(string url)
{
string pattern = ".*?q=([^&]+)*";
var matches = Regex.Matches(url, pattern, RegexOptions.None);
string stockName = matches[0].Groups[1].Value;
return stockName;
}
可视化获取的股票数据
在创建了两个类 - GoogleLiveStockDataQuerier
和 GoogleHistoricalStockDataQuerier
- 来查询和解析数据并在完成时引发事件后,我们拥有可视化股票所需的一切。
Visiblox Charts for Silverlight 概述
可视化将使用免费版的 Visiblox Charts for Silverlight。有几个 Silverlight 组件可用 - 最受欢迎的是开源的 Silverlight Toolkit 图表。我在此场景中选择此工具的原因是它具有内置的缩放和平移功能,并且在屏幕上渲染数百个点时具有不错的性能。
在此项目中,实时股票数据将作为图表的标题不断更新,而历史股票将呈现为折线图的点。
可视化历史和实时数据
要可视化数据,我们需要创建一个图表,添加一个折线图系列,执行数据源查询,并在查询完成后,更新历史数据的折线图系列和实时数据的图表标题。我还将添加平移和缩放功能,以帮助导航数据。创建图表
要创建图表,我们需要将 Chart
对象添加到可视化树中。我们还需要创建 LineSeries
来可视化检索到的数据。另外,在此示例中,我想使用 GoogleHistoricalStockDataQuerier
返回的 IList
列表来可视化历史股票数据,而无需做太多工作。因此,我将此业务对象集合绑定到折线图系列,方法是将 BindableDataSeries
设置为 LineSeries
的 DataSeries
属性,并在其上设置正确的 XValueBinding
和 YValueBinding
属性。这样,以后我只需将 ItemsSource
设置在 BindableDataSeries
上即可更新历史股票数据。
使用 XAML 或代码隐藏都可以创建带有数据系列绑定的 LineSeries
的 Chart
。在此示例中,我选择使用 XAML。
<UserControl x:Class="StockVisualization.MainPage"
(...)
xmlns:charts="clr-namespace:Visiblox.Charts;assembly=Visiblox.Charts"
>
<StackPanel x:Name="LayoutRoot" Orientation="Vertical">
<charts:Chart x:Name="StockChart" Width="600" Height="300"
LegendVisibility="Collapsed">
<charts:Chart.Series>
<!-- Add a LineSeries that shows points and displays
tooltips when hovering over them -->
<charts:LineSeries.DataSeries>
<!-- Set the data source of the LineSeries to be
a BindableDataSeries to allow binding to our business
object collection -->
<charts:BindableDataSeries XValueBinding="{Binding Date}"
YValueBinding="{Binding Price}"/>
</charts:LineSeries.DataSeries>
</charts:Chart.Series>
</StackPanel>
</UserControl>
创建输入控件
为了使示例正常工作,我们需要一种方式让用户指定要查询的股票类型。为简单起见,在此示例中我将使用一个带有预定义值的 ComboBox
。
查询实时和历史股票数据不是瞬间完成的,最好让用户知道后台正在发生一些事情。出于这个原因,我创建了一个 ProgressBar
来在查询进行时显示。
此示例中这些控件的 XAML 如下:
<StackPanel x:Name="LayoutRoot" Orientation="Vertical">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<TextBlock>Stock code:</TextBlock>
<ComboBox x:Name="ComboStockCode">
<ComboBoxItem Content="MSFT" IsSelected="True"/>
<ComboBoxItem Content="GOOG"/>
<ComboBoxItem Content="VOD"/>
<ComboBoxItem Content="BCS"/>
</ComboBox>
<Button x:Name="BtnUpdate" Content="Update" Click="BtnUpdate_Click"/>
</StackPanel>
<StackPanel Orientation="Horizontal" x:Name="PanelFetchingData"
Visibility="Collapsed" Margin="0,5,0,5" HorizontalAlignment="Center">
<ProgressBar IsIndeterminate="True" Width="150" Height="15"/>
</StackPanel>
<charts:Chart x:Name="StockChart" Width="600" Height="300"
LegendVisibility="Collapsed">
<!-- definition of the chart -->
</charts:Chart>
<TextBlock HorizontalAlignment="Center" Width="400"
TextWrapping="Wrap" Text="Drag mouse to pan, scroll with mousewheel to zoom."/>
</StackPanel>
可视化实时股票数据
可视化实时股票数据非常明显:我们所需要做的就是创建一个 GoogleLiveStockDataQuerier
实例并订阅其 OnDataUpdated
事件。当该事件触发时,我们将更新图表的 Title
。代码如下:
private GoogleLiveStockDataQuerier _liveDataQuerier;
public MainPage()
{
// Other initialization logic
_liveDataQuerier = new GoogleLiveStockDataQuerier();
_liveDataQuerier.OnDataUpdated +=new StockDataEventHandler
<livestockdata>(_liveDataQuerier_OnChanged);
// Other logic
}
void _liveDataQuerier_OnChanged(object sender, EventWithDataArgs<livestockdata> e)
{
if (e.Data != null)
{
Dispatcher.BeginInvoke(() =>
{
StockChart.Title = String.Format("{0}: {1} ({2})",
e.Data.StockCode, e.Data.Price, e.Data.Change);
}
);
}
}
可视化历史股票数据
可视化历史股票数据也非常简单。当用户单击“更新”按钮时,将调用 GoogleHistoricalStockDataQuerier
实例的 QueryData
方法。调用此方法时,它将引发一个 OnQueryCompleted
事件,该事件将在事件的 Data
成员中返回一个 IList
。然后,只需将此 IList
设置为我们在图表上定义的 BindableDataSeries
的 ItemsSource
即可。
所有这些代码都比解释的要简单得多,相关部分如下:
private GoogleHistoricalStockDataQuerier _historicDataQuerier;
public MainPage()
{
// Other initalization logic
_historicDataQuerier = new GoogleHistoricalStockDataQuerier();
_historicDataQuerier.OnQueryCompleted += new StockDataEventHandler
<ilist<hlochistoricstockdata>>(_historicDataQuerier_OnQueryCompleted);
// Load data in the chart on startup
UpdateChartData();
}
public void UpdateChartData()
{
// Clear the title of the chart
StockChart.Title = "";
// Show the loading indicator
PanelFetchingData.Visibility = Visibility.Visible;
var stockCode = (ComboStockCode.SelectedItem as ComboBoxItem).Content.ToString();
// Change the live updates to the new stock
_liveDataQuerier.Subscribe(stockCode);
// Query a quarter's data and set the chart to show the last 6 months
// (the rest can be panned)
_historicDataQuerier.QueryData(stockCode, DateTime.Now.AddMonths(-6), DateTime.Now);
}
void _historicDataQuerier_OnQueryCompleted(object sender, EventWithDataArgs
<IList<hlochistoricstockdata>> e)
{
// Hide the loading indicator
PanelFetchingData.Visibility = Visibility.Collapsed;
// Set the returned data as ItemsSource to the BindableDataSeries:
// this will update the series
(StockChart.Series[0].DataSeries as BindableDataSeries).ItemsSource = e.Data;
}
为图表添加交互性
到目前为止,该示例获取了过去半年的数据,并在图表上显示所有数据点。这意味着我们已经显示了大约 200 个数据点,这在某些点可能会有点拥挤。

在屏幕上显示较少的数据点并允许缩放和平移是有意义的,使用户能够以更具交互性的方式导航图表。
缩放和平移是 Visiblox 图表的行为。为了使用它们,只需将它们设置为图表的Behaviour
属性即可。由于我们将同时使用缩放和平移,因此我们需要添加一个 BehaviourManager
,它允许在图表上使用多个行为。这可以在 XAML 中完成:
<charts:Chart x:Name="StockChart" Width="600" Height="300"
LegendVisibility="Collapsed">
<charts:Chart.Behaviour>
<charts:BehaviourManager AllowMultipleEnabled="True">
<charts:PanBehaviour YPanEnabled="False" IsEnabled="True"/>
<charts:ZoomBehaviour IsEnabled="True"/>
</charts:BehaviourManager>
</charts:Chart.Behaviour>
</charts:Chart>
在上一个代码示例中,我们在 PanBehaviour
上将 YPanEnabled
设置为 false
,这意味着我们只能在 X 轴上进行平移 - 在这种情况下,允许用户上下平移 Y 轴没有多大意义。
添加缩放和平移后我们面临的唯一问题是,默认情况下所有点都显示在屏幕上,因此没有太多可以平移的。通过在 UpdateChartData
方法中将 X 轴的 Range
设置为仅显示上个月的点,我们可以改变这一点。为此,我们只需重新定义 X 轴的 Range
。
// If the X axis has not yet been initialized
// (that is we're calling UpdateChartData from the MainPage constructor)
// we'll have to manually assign it
if (StockChart.XAxis == null)
{
StockChart.XAxis = new DateTimeAxis();
}
StockChart.XAxis.Range = new DateTimeRange()
{ Minimum = DateTime.Now.AddMonths(-1), Maximum = DateTime.Now };
添加这几行代码后,现在可以通过拖动鼠标来平移图表,并通过使用鼠标滚轮来缩放图表。
这是编译后的示例的屏幕截图:

关于代码重用的思考
在本文中,我将可视化实时和历史股票数据的问题分解为 3 个主要步骤:
- 定义用于存储的数据结构和用于获取实时和历史数据的接口。
- 使用 Google Finance 实现股票数据的获取。
- 使用 Visiblox Charts for Silverlight 可视化获取的股票数据。
通过定义股票数据的结构并声明获取它们的接口,我的目标是允许轻松地将数据提供商更换为任何其他提供商(例如 Yahoo Finance 或具有 public
API 的其他服务)。
用于可视化数据的图表组件也可以轻松地替换为任何其他 Silverlight 图表库。在这种情况下,可视化实现将特定于所选组件。然而,使用任何图表库显示带有折线图或蜡烛图的图表应该不难。
结论
在本文中,我实现了一种相当通用的获取和可视化实时及历史股票数据的方法。在实现过程中,我使用了一个特定的数据提供商和特定的图表组件。该解决方案旨在轻松地更改数据源或数据可视化库。您可以 在此处下载源代码。
希望您觉得本文有用。欢迎在下方发表评论。
历史
- 2011 年 1 月 19 日:初稿