Crypto triangle arbitrage: how to part 2: frontend

In this article, we will create a quick and dirty crypto triangle arbitrage dashboard frontend with React, to display the triangle arbitrage calculation results and related data. The dashboard will connect to our backend, described in the article: Crypto triangle arbitrage dashboard: how to, part 1, using a WebSocket connection. Then, the triangle arbitrage data received from the backend will be displayed in a simple table.

The completed React project can be found on my GitHub: here.

Prerequisites

The following is required to be able to follow the article and run the frontend and receive data:

Creating the frontend project for the crypto triangle arbitrage dashboard

Let’s use the npx command to create a new project: npx create-react-app triangle-arbitrage-dashboard-frontend --template typescript.

We use the typescript template because we like type hinting.

First, clean up the generated code in the App.css and App.tsx files:

App.css

.App {
  text-align: center;
  background: black;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  font-size: calc(10px + 2vmin);
  color: white;
}

.title {
  font-size: calc(10px + 2vmin);
}

App.tsx

import React from 'react';
import './App.css';

function App() {
  return (
    <div className="App">
      <div className="title">
        Crypto triangle arbitrage dashboard
      </div>
    </div>
  );
}

export default App;

Triangle arbitrage data classes

Before we can receive data from our backend it helps to have the data mapped out into classes. The simplest way to set this up is to follow a similar structure to what the backend uses. Considering that, let us look at the structs our Rust backend uses. The following listing shows the contents of the models.rs file:

use serde::de;
use serde::{Deserialize, Deserializer, Serialize};

#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct OfferData {
    #[serde(deserialize_with = "de_float_from_str")]
    pub price: f64,
    #[serde(deserialize_with = "de_float_from_str")]
    pub size: f64,
}

#[derive(Debug, Deserialize, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct DepthStreamData {
    pub last_update_id: u32,
    pub bids: Vec<OfferData>,
    pub asks: Vec<OfferData>,
}

pub fn de_float_from_str<'a, D>(deserializer: D) -> Result<f64, D::Error>
where
    D: Deserializer<'a>,
{
    let str_val = String::deserialize(deserializer)?;
    str_val.parse::<f64>().map_err(de::Error::custom)
}

#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct DepthStreamWrapper {
    pub stream: String,
    pub data: DepthStreamData,
}

#[derive(Debug, Serialize, Clone)]
pub struct TriangleArbitrageData {
    pub triangle: [String; 3],
    pub profits: Vec<f64>,
    pub start_pair_data: DepthStreamWrapper,
    pub mid_pair_data: DepthStreamWrapper,
    pub end_pair_data: DepthStreamWrapper,
}

This is all pretty straightforward, and we can ignore the function on line 20. So, let’s create a directory called classes in our project’s src dir, and add a file ArbitrageData.ts. In this file, we will create the following classes:

class OfferData {
  price: number;
  size: number;

  constructor(price: number, size: number) {
    this.price = price;
    this.size = size;
  }
}

class DepthStreamData {
  last_update_id: number;
  asks: Array<OfferData>;
  bids: Array<OfferData>;

  constructor(
    last_update_id: number,
    asks: Array<OfferData>,
    bids: Array<OfferData>
  ) {
    this.last_update_id = last_update_id;
    this.asks = asks;
    this.bids = bids;
  }
}

class DepthStreamWrapper {
  stream: String;
  data: DepthStreamData;

  constructor(stream: String, data: DepthStreamData) {
    this.stream = stream;
    this.data = data;
  }
}

class TriangleArbitrageData {
  triangle: Array<String>;
  profits: Array<number>;
  start_pair_data: DepthStreamWrapper;
  mid_pair_data: DepthStreamWrapper;
  end_pair_data: DepthStreamWrapper;

  constructor(
    triangle: Array<String>,
    profits: Array<number>,
    start_pair_data: DepthStreamWrapper,
    mid_pair_data: DepthStreamWrapper,
    end_pair_data: DepthStreamWrapper
  ) {
    this.triangle = triangle;
    this.profits = profits;
    this.start_pair_data = start_pair_data;
    this.mid_pair_data = mid_pair_data;
    this.end_pair_data = end_pair_data;
  }
}

export { OfferData, DepthStreamData, DepthStreamWrapper, TriangleArbitrageData };

The data structure is pretty much one for one converted, aside from some different types.

Connecting to the crypto triangle arbitrage backend WebSocket

In this section, we will add functionality to connect to the backend using a WebSocket connection.

Before we get into displaying the data in a table, let’s first write code that establishes a connection and add a simple way to confirm that we are receiving data.

Here is the updated code for App.tsx:

import React, { useState } from "react";
import "./App.css";

let ws: WebSocket;

function App() {
  const [connected, setConnected] = useState<boolean>(false);

  function onOpen(event: any): void {
    console.log("Opened WebSocket connection.");
    setConnected(true);
  }

  function onMessage(event: any): void {
    let parsedData = JSON.parse(event.data);
    console.log("received data for: " + parsedData.triangle);
  }

  function connectToBackend() {
    console.log("Connecting to backend..");
    ws = new WebSocket("ws://127.0.0.1:8000/ws");
    ws.onopen = onOpen;
    ws.onmessage = onMessage;
  }

  function disconnectFromBackend() {
    console.log("Disconnected.");
    if (ws) {
      ws.close();
    }
    setConnected(false);
  }

  return (
    <div className="App">
      <div>Crypto triangle arbitrage dashboard</div>
      <div>
        {connected ? (
          <button onClick={disconnectFromBackend}>Disconnect</button>
        ) : (
          <button onClick={connectToBackend}>Connect</button>
        )}
      </div>
    </div>
  );
}

export default App;

This will show a connect button in the interface when we are not yet connected and a disconnect button when we are connected. The connect button calls connectToBackend which will create a new WebSocket object to connect to the locally running crypto triangle arbitrage dashboard backend. We should make sure to start the backend before clicking this button.

We have implemented two event handler functions: onOpen and onMessage. The onOpen function will be called when the connection is established successfully and just prints a message and sets connected to true:

  function onOpen(event: any): void {
    console.log("Opened WebSocket connection.");
    setConnected(true);
  }

Next, the onMessage function handles all incoming message events. We will add more to this function in the next section to handle the triangle arbitrage data. Currently, it just prints a small part of the data, the coin symbols used in the triangle, to the console:

  function onMessage(event: any): void {
    let parsedData = JSON.parse(event.data);
    console.log("received data for: " + parsedData.triangle);
  }

With all this code in place, start the backend and frontend projects, then click the connect button. Opening up the dev tools of your browser and looking at the console output should look something like this:

Crypto triangle arbitrage dashboard backend connection success!
Crypto triangle arbitrage dashboard backend connection success!

Displaying the crypto triangle arbitrage data

Finally, let’s process the data and display it in a simple table.

To make the data table look a bit presentable let’s update the App.css:

.App {
  text-align: center;
  background: black;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  
  color: white;
}

.title {
  font-size: calc(10px + 2vmin);
}

table, td, th {
  border: 1px solid white;
}

td {
  padding: 5px 10px;
}

.profit {
  color: rgb(31, 160, 31);
}

.loss {
  color: rgb(221, 49, 49);
}

Then we can update App.tsx to handle and display the arbitrage data.

Update the imports to import the TriangleArbitrageData class:

import React, { useState } from "react";
import "./App.css";
import { TriangleArbitrageData } from "./classes/ArbitrageData";

Adding a new useState line for holding and setting the data:

function App() {
  const [connected, setConnected] = useState<boolean>(false);
  const [triangleArbitrageData, setTriangleArbitrageData] = useState<TriangleArbitrageData>();

Updating the onMessage to parse data to a TriangleArbitrageData object, then updating the state variable:

function onMessage(event: any): void {
    let parsedData: TriangleArbitrageData = JSON.parse(event.data);
    console.log("received data for: " + parsedData.triangle + 
    ": " + parsedData.profits);

    setTriangleArbitrageData(parsedData);
}

Now, let’s write a function to render the data table:

function renderTriangleArbitrageDataTable() {
    return (
      <div>
        <table>
          <thead>
            <tr>
              <th>triangle</th>
              <th>potential profit</th>
            </tr>
          </thead>
          <tbody>
            {triangleArbitrageData?.profits.map( (triangleProfit, i) => {
              return (
                <tr key={i}>
                  <td>
                    {triangleArbitrageData.triangle}
                  </td>
                  <td className={triangleProfit > 0 ? "profit" : triangleProfit === 0 ? "" : "loss"  }>
                    {triangleProfit}
                  </td>
                </tr>
              )
            } ) }
          </tbody>
        </table>
      </div>
    ); 
 }

Just a simple table where the rows are generated from the profits array using the map function. We set the className based on if the profit number is positive or negative or 0.

Finally, we have to update the main HTML rendering:

return (
    <div className="App">
      <div className="title">Crypto triangle arbitrage dashboard</div>
      <div>
        {connected ? (
          <button onClick={disconnectFromBackend}>Disconnect</button>
        ) : (
          <button onClick={connectToBackend}>Connect</button>
        )}
      </div>
      <div>
          {triangleArbitrageData ? renderTriangleArbitrageDataTable() : <div>No data to show, please connect to backend.</div> }
      </div>
    </div>
 );

The end result

Having updated all that we can run the backend again and connect to the frontend to see the data coming in and being updated in the UI:

Crypto triangle arbitrage dashboard frontend
Crypto triangle arbitrage dashboard frontend

Unfortunately, it was all red this time!

Conclusion

We have completed our simple frontend for our crypto triangle arbitrage dashboard. From this, we learned how to connect to our backend using a WebSocket, and how easy it is to convert the incoming data to an object for display.

The frontend helps us visually see potential profit opportunities. However, we are only showing one triangle here. The next step for the backend and frontend could be to get and show data for many different triangles.

Please check out the completed React project on my GitHub: here.

Part 1 of this series, where the creation of the backend is explained: Crypto triangle arbitrage dashboard: how to, part 1.

The next part is also available: Crypto triangle arbitrage dashboard, part 3: how to add logging

Comments (2)
  • Hello!

    I’ve really enjoyed your blog. Thank you for posting.

    This project uses react for the front end. React seems to add a lot of overhead to the project. Would it be possible to use a rust framework like rocket, yew or other to make the frontend?

    • Hi,
      Thank you for your feedback. I appreciate it.
      Yes, you can use any frontend framework you want to in combination with the backend. I have to say I am very interested in trying out Rust-and-wasm-based frontend frameworks but have very little experience with those. I will definitely investigate those options and write about them in the future. Thanks for the suggestion.

      However, I also believe React or other modern JavaScript frameworks can be quite fast as well. The frontend in the article isn’t optimized and doesn’t use cached rendering techniques. So it could definitely be improved upon.

Leave a Reply

Your email address will not be published.