Introduction
The world is changing. We want information as soon as possible without having to keep asking for it. Websockets allow us to do just that.
In this guide, you will learn how to create an API that listens to a websocket and pushes that information to subscribers.
I am going to be implementing Coinigy's API which provides real-time feeds for trade history, orderbook data, and blockchain alerts. In this example, we will only be looking at trade information.
The code for this article can be downloaded here.
This article is the first part in the series.
Part 2 can be found here: Implement a Websocket API with Owin and SignalR [Part 2].
Prerequisites
- Visual Studio 2015 or 2017 with Framework 4.6.1
- A
Coinigy
API key and secret
Table of Contents
- Getting your API Key
- Using the code
- Step 01 - Create a Class Library
- Step 02 - Install Nuget packages
- Step 03 - Create a
Websocket
class - Step 04 - Authenticate
- Step 05 - Subscribe to trade channel
- Step 06 - Parse messages
- Step 07 - Deserialize trade response
- Step 08 - Connect to the socket
- Step 09 - Testing
- Final words
Getting Your API Key
Note that this article can be easily adapted to use any other API.
Once you are logged in to Coinigy, go the Settings > My Account > Coinigy API and click on the "Generate New Key" button. This will create a new key and secret you will use within the API.
Keep your credentials safe!
Using the Code
Step 01 - Create a Class Library
Open Visual Studio and go to File > New > Project and select Class Library.
Ensure your framework targets .NET Framework 4.6.1.
Give your project a name (e.g. Coinigy.API) and click "OK".
Step 02 - Install Nuget Packages
In the package manager console, install the following packages using these commands:
Install-Package PureSocketCluster
Step 03 - Create a Websocket Class
We need to be able to authenticate with our socket.
Add a "Models" folder to your project (right-click your project, Add > New Folder).
This folder will contain all our models. We can now add an ApiCredentials
model under this folder:
public class ApiCredentials
{
[JsonProperty("apiKey")]
public string ApiKey { get; set; }
[JsonProperty("apiSecret")]
public string ApiSecret { get; set; }
}
Rename your "Class1.cs" file to "Websocket.cs".
Add the following two fields to your Websocket
class:
private readonly PureSocketClusterSocket socket;
private readonly ApiCredentials credentials;
Add a constructor to the Websocket
class:
public Websocket(ApiCredentials credentials)
{
this.credentials = credentials;
this.socket = new PureSocketClusterSocket("wss://sc-02.coinigy.com/socketcluster/");
}
The constructor allows the user to pass in their credentials and establishes a connection to Coingiy's websocket
URI.
Step 04 - Authenticate
The websocket will send information to us but we first need to subscribe to these events. We first need to authenticate with the socket. We will start receiving information from the socket after the authentication succeeds. To authenticate with the webservice, we need to send the "auth
" command along with our credentials. The "auth
" command can only be called once the socket has opened. Therefore, we need to subscribe to the "OnOpened
"-event. This is how we subscribe to the event:
this.socket.OnOpened += On_Opened;
And the function looks like this:
private void On_Opened()
{
socket.Emit("auth", this.credentials, ack: (string name, object error, object data) =>
{
});
}
Step 05 - Subscribe to Trade Channel
To subscribe to a trade channel, we can add the following function to our Websocket
-class:
public bool SubscribeToTradeChannel(string exchange, string primaryCurrency,
string secondaryCurrency) => this.socket.Subscribe
($"TRADE-{exchange}--{primaryCurrency}--{secondaryCurrency}");
This function will be called after we have authenticated. We need to let the user know when authentication has completed so let's add an event:
public event ClientIsReady OnClientReady;
public delegate void ClientIsReady();
And invoke it when authentication has succeeded within our Emit
-callback:
private void On_Opened()
{
socket.Emit("auth", this.credentials, ack: (string name, object error, object data) =>
{
OnClientReady?.Invoke();
});
}
Now the user can subscribe to the OnClientReady
-event and start subscribing to trade channels once it gets invoked.
Note: Coinigy's channel names need to be in the following format: METHOD-EXCHANGECODE--PRIMARYCURRENCY--SECONDARYCURRENCY
Step 06 - Parse Messages
Now we are subscribed but we still need to create functionality to receive messages being sent to us from the socket. We need to subscribe to the OnMessage
-event:
this.socket.OnMessage += On_Message;
And the function looks like this:
private void On_Message(string message)
{
}
Every message we receive from the socket will be invoked here. We still need to determine what type of message the server has sent us since it can return different types of messsages. To determine the type, we can add the following function:
private static string GetRequestType(JObject jObj)
{
string requestType = string.Empty;
var channelName = jObj["data"]["channel"].ToString();
Guid guid;
if (!Guid.TryParse(channelName, out guid))
return channelName.Substring(0, channelName.IndexOf('-'));
Guid channelGuid;
requestType = channelName;
if (Guid.TryParse(channelName, out channelGuid))
if (channelGuid.ToString().ToLower() == channelName.ToLower())
requestType = jObj["data"]["data"]["MessageType"].ToString();
return requestType;
}
When we receive a message, we first need to check if it is a "publish
" message. Once the message has been verified, we can parse it into a JObject
and send it to our GetRequestType
-function to determine the type of request. Add the following logic to the On_Message
-function:
string PUBLISH_REGEX = @"^{""event""*.:*.""#publish""";
var m = Regex.Match(message, PUBLISH_REGEX);
if (!m.Success) return;
var jObj = JObject.Parse(message);
string channelName = jObj["data"]["channel"].ToString();
string requestType = GetRequestType(jObj);
if (string.IsNullOrEmpty(requestType))
return;
Once we know what the request type is, we need to parse the message to that type and send it to the user. There are different message types so let's add an enum
for that:
public enum MessageType
{
TradeData,
OrderData,
NewsData,
BlockData,
FavoriteData,
NewMarket,
NotificationData,
Unknown
}
And a function to determine what the message type is based on the request type:
private static MessageType GetMessageType(string requestType)
{
switch (requestType.ToUpper())
{
case "ORDER":
return MessageType.OrderData;
case "TRADE":
return MessageType.TradeData;
case "BLOCK":
return MessageType.BlockData;
case "FAVORITE":
return MessageType.FavoriteData;
case "NOTIFICATION":
return MessageType.NotificationData;
case "NEWS":
return MessageType.NewsData;
case "NEWMARKET":
return MessageType.NewMarket;
default:
return MessageType.Unknown;
}
}
This function takes the request type as parameter and returns our message type. Once we determine the message type, we can use a switch
statement to parse and return the data. Update the OnMessage
-function:
private void On_Message(string message)
{
InvokeMessageReceived(channelName, requestType, message);
}
And add the InvokeMessageReceived
-function:
private void InvokeMessageReceived(string channelName, string requestType, string message)
{
MessageType messageType = GetMessageType(requestType);
switch (messageType)
{
case MessageType.TradeData:
var tradeMarketInfo = MarketInfo.ParseMarketInfo(channelName);
var trade = Helper.ToEntity<TradeResponse>(message);
OnTradeMessage?.Invoke(tradeMarketInfo.Exchange, tradeMarketInfo.PrimaryCurrency,
tradeMarketInfo.SecondaryCurrency, trade.TradeData.Trade);
break;
}
}
We still need to add a few functions to parse the message into a trade data entity. Let's create a class with a function which takes a channel name as parameter and returns a MarketInfo
-entity (we can add this class into our Models-folder).
internal class MarketInfo
{
internal string Exchange { get; set; }
internal string PrimaryCurrency { get; set; }
internal string SecondaryCurrency { get; set; }
internal static MarketInfo ParseMarketInfo(string data)
{
var str = data.Replace("--", "-");
var strArr = str.Split('-');
return new MarketInfo() { Exchange = strArr[1], PrimaryCurrency = strArr[2],
SecondaryCurrency = strArr[3] };
}
}
Now we know what the exchange is and what our primary/secondary currencies are. We still need to parse our trade info using the message. Let's create a TradeResponse
-entity that will contain all that info:
public class TradeResponse
{
[JsonProperty("data")]
public TradeData TradeData { get; set; }
[JsonProperty("event")]
public string Event { get; set; }
}
public class TradeData
{
[JsonProperty("channel")]
public string Channel { get; set; }
[JsonProperty("data")]
public TradeItem Trade { get; set; }
}
public class TradeItem
{
[JsonProperty("label")]
public string Label { get; set; }
[JsonProperty("quantity")]
public decimal Quantity { get; set; }
[JsonProperty("exchId")]
public long ExchId { get; set; }
[JsonProperty("channel")]
public string Channel { get; set; }
[JsonProperty("exchange")]
public string Exchange { get; set; }
[JsonProperty("marketid")]
public long Marketid { get; set; }
[JsonProperty("market_history_id")]
public long MarketHistoryId { get; set; }
[JsonProperty("price")]
public decimal Price { get; set; }
[JsonProperty("time_local")]
public string TimeLocal { get; set; }
[JsonProperty("total")]
public decimal Total { get; set; }
[JsonProperty("time")]
public string Time { get; set; }
[JsonProperty("timestamp")]
public string Timestamp { get; set; }
[JsonProperty("tradeid")]
public string TradeId { get; set; }
[JsonProperty("type")]
public string Type { get; set; }
}
Step 07 - Deserialize Trade Response
We still need to deserialize our message into a TradeResponse
. Let's add a Helper
class with a generic "ToEntity
" function that takes a string
as parameter and returns an entity:
internal static class Helper
{
internal static T ToEntity<T>(string data)
{
return Newtonsoft.Json.JsonConvert.DeserializeObject<T>(data);
}
}
Now we have deserialized our trade market data and need to let the user know. Let's add an event the user can subscribe to:
public event TradeMessage OnTradeMessage;
public delegate void TradeMessage(string exchange, string primaryCurrency,
string secondaryCurreny, TradeItem trade);
Step 08 - Connect to the Socket
The last function we need to add is to allow the user to connect to the socket with the following code:
public bool Connect()
{
return this.socket.Connect();
}
Step 09 - Testing
We are now ready to receive trade messages. Let's test our implementation with a unit test project. Right-click on the solution and select Add > New Project > Test > Unit Test Project and give it a name (i.e., Coinigy.API.Tests
).
We need to create a class to test our trade message functionality. Add a new "WebsocketTests
" class to the test project:
[TestClass]
public class WebsocketTests
{
private static readonly ManualResetEvent resetEvent = new ManualResetEvent(false);
private static Websocket socket;
[TestMethod]
public void Subscribe_And_Listen()
{
socket = new Websocket(new ApiCredentials
{
ApiKey = "[YOUR-API-KEY]",
ApiSecret = "[YOUR-API-SECRET]"
});
socket.OnClientReady += Socket_OnClientReady;
socket.OnTradeMessage += Socket_OnTradeMessage;
socket.Connect();
resetEvent.WaitOne();
}
private void WriteLog(string message)
{
Debug.WriteLine($"{DateTime.UtcNow}: {message}");
}
private void Socket_OnTradeMessage(string exchange, string primaryCurrency,
string secondaryCurrency, Models.TradeItem trade)
{
WriteLog($"Received new trade for {exchange} market
{primaryCurrency}/{secondaryCurrency} price {trade.Price}");
}
private void Socket_OnClientReady()
{
socket.SubscribeToTradeChannel("BMEX", "XBT", "USD");
}
}
We need to add a reference to our API. Right-click References under the unit test project and select "Add Reference" > "Project" and select the API project. Finally, we need to install one last package into our unit test project. Run the following command:
"Install-Package PureSocketCluster"
When we run our test (Ctrl+R, Ctrl+A), we should start seeing trade messages in our Output window:
02-Dec-17 8:04:09 PM: Received new trade for BMEX market XBT/USD price 10939.5
02-Dec-17 8:04:09 PM: Received new trade for BMEX market XBT/USD price 10939.5
02-Dec-17 8:04:09 PM: Received new trade for BMEX market XBT/USD price 10940
02-Dec-17 8:04:10 PM: Received new trade for BMEX market XBT/USD price 10940
Final Words
We can now receive live trade data from the Coinigy
API. We can even take it a step further and implement this API into a web application which shows us some statistical information. That will be our next project. Thanks for reading!