Build

Real-time Currency Tracker with PubNub C# SDK

Michael Carroll on May 24, 2016
Real-time Currency Tracker with PubNub C# SDK

PubNub’s real-time messaging infrastructure has proved its mettle across various industry domains, Fintech (Financial Technology) being one of them. From instant stock updates to real-time financial data analytics, PubNub provides a resilient and best in class network for streaming financial data across applications. In this blog post, we are going to demonstrate a small Fintech demo application which is a micro dashboard for streaming currency exchange rates in real time using the PubNub C# SDK.

We have named this application, “CurrencyTracker” and the currency data is fetched from the CurrencyLayer API, which provides a public API service for getting reliable exchange rates and currency conversions data.

Currency Tracker Diagram

The CurrencyTracker server application is a C# / .NET application. The client is a web based dashboard built in HTML5 / JavaScript. This project uses the following PubNub SDKs for enabling real-time messaging between server and client:

The entire source code of this demo is available in this repository on GitHub.

Server Setup

The server application is built on top of the .NET application template available in Visual Studio. The source code of solution is in the server/CurrencyTrack folder in the GitHub repo and can be opened with Visual Studio.

The steps for configuring the server application are mentioned in README file.

CurrencyLayer API Setup

In order to get currency data, we have to sign up for currencylayer.com API access. It provides a free plan which meets our requirements.

For this application we have chosen to display four currencies against USD (United States Dollar). These are EUR (Euro) , AUD (Australian Dollar) , CYN (Chinese Yuan) and INR (Indian Rupee)

The API endpoint and its parameters for getting these currency values is as follows

http://apilayer.net/api/live?access_key=< your_api_key >&currencies=EUR,AUD,CNY,INR&source=USD&format=1

Where < your_api_key > is the API key that you get after signup which is provided by currencylayer.com for accessing the service.

Server Logic

PubNub and CurrencyLayer Initialization

This application relies on PubNub and Currencylayer service, so the first thing to be done after starting the application is to initialize PubNub and Currencylayer API as follows.

void PNInit()
{
    _pubnub = new Pubnub(System.Web.Configuration.WebConfigurationManager.AppSettings["PNPubKey"],
   System.Web.Configuration.WebConfigurationManager.AppSettings["PNSubKey"]);         
}
 const string url = "http://apilayer.net/api/live?access_key=1a142e188a7b4a43e404eee3bbf52378&currencies=EUR,AUD,CNY,INR&source=USD&format=1"; // URL of currencylayer site

Routine Polling of Currencylayer API

The server application polls the CurrencyLayer API every one hour to fetch the latest currency rates. For this, the application uses the Quartz library available under C#.

using System;
using System.Collections.Generic;c
using System.Linq;
using System.Web;
using Quartz;// For Schedule Task
using Quartz.Impl;
using Quartz.Impl.Triggers;
namespace CurrencyTrack
{
    public class JobScheduler : ScheduledTask
    {
        /// <summary>
        /// The scheduleTask is called at an regular interval of 30 seconds.
        /// </summary>
        public static void Start()
        {
            IScheduler scheduler = StdSchedulerFactory.GetDefaultScheduler();
            scheduler.Start();
            IJobDetail job = JobBuilder.Create<ScheduledTask>().Build();
            ITrigger trigger = TriggerBuilder.Create()
                                    .WithIdentity("trigger1", "group1")
                                    .StartNow()
                                    .WithSimpleSchedule
                                        (s =>l;
                                            s.WithIntervalInMinutes(60)
                                            .RepeatForever()
                                         ).Build();
            scheduler.ScheduleJob(job, trigger);
        }// End method Start()
    }// End JobScheduler
}

Once the data is obtained from Currencylayer API, the server checks for changes in currency values from the previous update. Once it is validated that there is a change in the data, the server stores the new currency values in an internal list and publishes the data on a PubNub channel for updating all client dashboards.

void StoreAndPublishCurrencyData(string currencyLabel, Dictionary<string, decimal> currencyObj , int TimeStamp )
        {
            int arrayCount = 0;
            bool isSameValue = false;
            //Get the count of historical values stored
            arrayCount = Global.ArrayDictMap[currencyLabel].Count;// getting the length of an array(for USDEUR, USDAUD, USDCNY, USDINR)
            if (arrayCount >= 1)
            {
                isSameValue = Global.ArrayDictMap[currencyLabel][arrayCount - 1] == currencyObj[currencyLabel];
            }
            if (!isSameValue)
            {
                if (arrayCount >= 30)
                {
                    Global.ArrayDictMap[currencyLabel].RemoveAt(0);
                }
                
                CurrencyData currencyDataPublish = new CurrencyData();
                currencyDataPublish.name = currencyLabel.Substring(3);
                currencyDataPublish.value = currencyObj[currencyLabel].ToString();
                currencyDataPublish.time = TimeStamp;
                try {
                    if (currencyObj[currencyLabel] > Global.ArrayDictMap[currencyLabel][arrayCount - 1])
                    {
                        currencyDataPublish.direction = "+";
                    }
                    else
                    {
                        currencyDataPublish.direction = "-";
                    }
                    currencyDataPublish.magnitude = Math.Abs(currencyObj[currencyLabel] - Global.ArrayDictMap[currencyLabel][arrayCount - 1]);
                }
                catch(ArgumentOutOfRangeException e)
                {
                    Console.WriteLine(e.ToString());
                    Console.WriteLine("Can be ignored if list has one or less elements");
                    currencyDataPublish.direction = "+";
                    currencyDataPublish.magnitude = currencyObj[currencyLabel];
                }
                
                Global.ArrayDictMap[currencyLabel].Add(currencyObj[currencyLabel]);
                string json_Publish = JsonConvert.SerializeObject(currencyDataPublish);
                Global._pubnub.Publish<string>(strCHANNELNAME, json_Publish, DisplayReturnMessage, DisplayErrorMessage);
                
            }
        }

The currency data is stored in the Global class under a set of Lists, each designated for the individual currencies.

public static List<decimal> arrUSDEUR = new List<decimal>();// globally declaring list for USDEUR
public static List<decimal> arrUSDAUD = new List<decimal>();// globally declaring list for USDAUD
public static List<decimal> arrUSDCNY = new List<decimal>();// globally declaring list for USDCNY
public static List<decimal> arrUSDINR = new List<decimal>();// globally declaring list for USDINR
       
public static Dictionary<string, List<decimal>> ArrayDictMap = new Dictionary<string, List<decimal>>()
{
    { "USDEUR", arrUSDEUR },
    { "USDAUD", arrUSDAUD },
    { "USDCNY", arrUSDCNY },
    { "USDINR", arrUSDINR },
};

Handling App Requests

The server also needs to handle requests from client dashboard for fetching currency values, either in the form of a counter or trend format. Counter format is used to send the latest currency rate and trend format is used to send the historical trend containing the last 30 values.

A PubNub channel named appRequestChannel is used for this purpose. Based on the request, the server retrieves the relevant data from the internal lists and publishes that on the common PubNub channel exchangedata

/// <summary>
/// Publish Latest Counter data in response to app request = 0
/// </summary>
/// <param name="pCurr">Currency Name</param>
void PublishCounter(string pCurr)
{
    string trendJSON = string.Empty;
    string currKey = "USD" + pCurr;
    int arrayCount = Global.ArrayDictMap[currKey].Count;
    if (arrayCount > 0)
    {
                
        CurrencyData currencyDataPublish = new CurrencyData();
        currencyDataPublish.name = pCurr;
        currencyDataPublish.value = ArrayDictMap[currKey][arrayCount - 1].ToString();
        currencyDataPublish.time = LastTimeStamp;
        try
        {
            if (Global.ArrayDictMap[currKey][arrayCount - 1] > Global.ArrayDictMap[currKey][arrayCount - 2])
            {
                currencyDataPublish.direction = "+";
            }
            else
            {
                currencyDataPublish.direction = "-";
            }
            currencyDataPublish.magnitude = Math.Abs(Global.ArrayDictMap[currKey][arrayCount - 1] - Global.ArrayDictMap[currKey][arrayCount - 2]);
        }
        catch (ArgumentOutOfRangeException e)
        {
            Console.WriteLine(e.ToString());
            Console.WriteLine("Can be ignored if list has one or less elements");
            currencyDataPublish.direction = "+";
            currencyDataPublish.magnitude = Global.ArrayDictMap[currKey][arrayCount - 1];
        }
        trendJSON = JsonConvert.SerializeObject(currencyDataPublish);
        _pubnub.Publish<string>(respChannel, trendJSON, DisplayReturnMessage, DisplayErrorMessage);
    }
}//End of PublishCounter
/// <summary>
/// Publish Trend data in response to app request = 1
/// </summary>
/// <param name="pCurr">Currency Name</param>
void PublishTrend(string pCurr)
{
    string trendJSON = string.Empty;
    string currKey = "USD" + pCurr;
    CurrencyTrendData currencyDataPublish = new CurrencyTrendData();
    currencyDataPublish.name = pCurr;
    currencyDataPublish.value = ArrayDictMap[currKey];
    currencyDataPublish.time = LastTimeStamp;
    trendJSON = JsonConvert.SerializeObject(currencyDataPublish); 
            
    _pubnub.Publish<string>(respChannel, trendJSON, DisplayReturnMessage, DisplayErrorMessage);
}//End of PublishTrend

Data Format

Data format of app request is as follows

{"name": "EUR" , "requestType":1}

Here ‘name’ contains the currency code for which the request is being sent and ‘requestType’ contains the value 0 (for counter) and 1 (or trend)

Based on request type, the server can send a message response for counter whose data format is:

{"responseType":0,"name":"EUR","value":1.33,"direction":"+","magnitude":0.00345,"time":1412322567}

Or, a message response for trend as follows:

{"responseType":1,"name":"EUR","value": [1.36,1.34,1.37,1.28,1.33] ,"direction":"+","magnitude":0.00365,"time":1412322567}

Here the responseType contains the value 0 or 1 to match the requestType parameter of request message, for either counter or the trend . For counter response, the value parameter contains the latest currency value and for trend response, it contains an array of historical values for the currency rate.

The direction parameter contains either a ‘+’ or ‘-’ character to indicate increase or decrease from the previous update and ‘magnitude’ parameter contains the delta amount of difference between the previous and current value of currency. Finally the ‘time’ parameter contains the unix timestamp of the currency value update from CurrencyLayer API.

Client Setup

Client dashboard is built on top of simple HTML5 & JavaScript, as a web app. The main UI for the client dashboard looks like this.

Client dashboard main UI

Since it relies on PubNub for delivering messages from the server, we need to initialize the PubNub JavaScript SDK in the client. At the beginning, the client initializes PubNub and also sends a request to server to get the latest values for all the currencies to be displayed in the dashboard.

var pubnub = PUBNUB.init({
    publish_key: pub_key,
    subscribe_key: sub_key
});
$(document ).ready(function() {
	pubnub.subscribe({
	    channel: 'exchangedata',
	    message: updatePrice
	});
    sendRequest("EUR",0);
    sendRequest("AUD",0);
    sendRequest("CNY",0);
    sendRequest("INR",0);
	$('button').click(function(){
        if($(this).text() == BUTTON_TXT_TREND){
            //If the Button text is 'Trend' , send request to fetch historical values 
            $(this).text('Loading..');
            sendRequest($(this).data('index'),1);
            counterDisplayState[$(this).data('index')] = false;
        } else if($(this).text() == BUTTON_TXT_COUNTER) {
            //Change the text
            counterDisplayState[$(this).data('index')] = true;
            $(this).text(BUTTON_TXT_TREND);
            sendRequest($(this).data('index'),0);
        }
    });
});

Upon receiving a response, the client javascript code updates the individual HTML elements for showing the currency value, the direction, magnitude and update time.

var updatePrice = function(p_rcvmessage){
    var p_message = JSON.parse(p_rcvmessage);
    if(p_message.responseType == 0){
        if(counterDisplayState[p_message.name] == true){
            $('#' + p_message['name']).html(parseFloat(p_message['value']).toFixed(2))
            valueChange(p_message['name'],p_message['direction'],parseFloat(p_message['magnitude']).toFixed(5))
            var date = new Date(p_message['time'] * 1000);
            var timeString = date.toLocaleTimeString();
            $('#' + p_message['name'] + '-time' ).html(timeString);
        }
        else{
            if(trend_graph[p_message.name] != null){
                valueChange(p_message.name,p_message['direction'],parseFloat(p_message['magnitude']).toFixed(5))
                trend_graph[p_message.name].shift();
                trend_graph[p_message.name].push(p_message.value);
                $('#'+p_message.name).sparkline(trend_graph[p_message.name])
                var date = new Date(p_message['time'] * 1000);
                var timeString = date.toLocaleTimeString();
                $('#' + p_message['name'] + '-time' ).html(timeString);
                $('[data-index='+p_message.name+']').text(BUTTON_TXT_COUNTER);
            }
        }
    }
	else if(p_message.responseType == 1){
        if(counterDisplayState[p_message.name] == false){
            trend_graph[p_message['name']] = p_message['value'];
            $('#'+p_message.name).sparkline(trend_graph[p_message.name])
            $('[data-index='+p_message.name+']').text(BUTTON_TXT_COUNTER);
            
        }
    }
};

If the user wants to see the trend for a currency, the ‘Trend’ button displayed against each currency can be clicked. This sends a request to the server and fetches the trend data which is displayed as shown below (for USD => INR).

Fetching the currency Trends

When trend is displayed for a specific currency, the user can switch back to the counter display by clicking the same button (which is labelled as “Counter” during the trend display). This also triggers a request from client to server to fetch the latest counter data for the currency.

Thinking Beyond

Fintech has emerged as one of the most promising industries in the recent years, thanks to smartphones. The app ecosystem is already flooded with many financial apps. For a foreign exchange trader, having this kind of micro dashboard in the form of a mobile app can empower users to keep a real close tab on the ever changing forex rates. Add to this some PubNub real-time capabilities, for example, rate fluctuations notifications, messaging for currency buying/selling, and we have a swiss army knife of forex trading.

Stay tuned for more such exciting demos on PubNub with different industry use cases.