Real-Time Crypto Price Tracker with Golang and Svelte

Rabinson ThapaRabinson Thapa
10 min read

Introduction

This project demonstrates how to leverage the WebSocket API to access live order book data for the BTC/USDT trading pair on Binance. You will learn how to calculate the average prices of bids and asks, and display the results on a simple web interface.

By the end of this blog, you will know how to connect to WebSocket, process real-time data using Golang, and create a dynamic frontend using Svelte. We will also explore how WebSocket communication operates and how to manage data broadcasting between the backend and multiple frontend clients.


What You Will Learn

Through this project, you’ll get hands-on experience with a variety of technologies, covering both backend and frontend development. Specifically, you will learn:

  • Connecting to WebSocket API: You’ll learn how to subscribe to real-time WebSockets, in this case, the order book for the BTC/USDT pair, and receive updates as they happen.

  • Broadcasting Data via WebSocket: You’ll explore how to set up a WebSocket server in Golang that can broadcast the calculated average price to connected clients.

  • Frontend with Svelte: On the frontend, you’ll learn how to establish WebSocket connections to receive and display live data in a user-friendly manner. We’ll also handle WebSocket reconnections gracefully.

  • Real-Time Data Display: Finally, you’ll learn how to display real-time price updates dynamically in a simple table using Svelte’s reactive features.

This project is a great way to deepen your understanding of real-time communication between backend and frontend, and how to build a lightweight real-time application.


What is This Project?

This project integrates Golang for the backend and Svelte for the frontend to develop a real-time order book price display system. The backend connects to Binance’s WebSocket API, monitors depth updates for the BTC/USDT trading pair, and calculates the average price from the bids and asks.

After calculating the average price, it is transmitted to all connected clients using WebSocket. On the frontend, we utilize Svelte to create a dynamic user interface that displays the latest prices as they update in real time. The frontend automatically reconnects if the connection is lost, ensuring a smooth and uninterrupted experience for users.

This project simulates a real-time data pipeline that you might encounter while developing tools for financial markets. Whether you are monitoring cryptocurrency prices, building trading bots, or simply learning about WebSocket and real-time web applications, this project will provide you with a solid foundation.

Prerequisites

Before diving into the project, it's essential to ensure you have the necessary skills and tools. Here’s what you’ll need:

  1. Basic knowledge of Golang.

  2. Understanding of WebSockets.

  3. Basic frontend development skills.

  4. Tools and Environment:

    • Golang installed on your machine.

    • Node.js and npm (Node Package Manager) installed for running the frontend Svelte application.

    • A code editor like Visual Studio Code

    • Basic knowledge of the terminal/command line for installing dependencies and running the project.

Backend setup

This section will walk you through setting up the backend, connecting to Binance WebSocket for real-time data, calculating average prices from the order book, and broadcasting the results to connected WebSocket clients.

1. Setting Up the Backend Project

Start by setting up the basic structure of your backend in Go. If you haven't already, follow these steps to initialize your project.

  1. Create the project directory: First, create a directory for your backend code.

     mkdir crypto-price-stream-backend
     cd crypto-price-stream-backend
    
  2. Initialize the Go module: Initialize your Go project using the following command:

     go mod init crypto-price-stream-backend
    
  3. Install dependencies: For this project, we’ll use the gorilla/websocket package to handle WebSocket communication. Install it using:

     go get github.com/gorilla/websocket
    

Now, your backend project is set up with the necessary structure and dependencies.


2. Connecting to Binance WebSocket

Next, we need to establish a WebSocket connection to Binance to receive real-time depth updates (order book data) for the BTC/USDT trading pair. This data will be processed later to calculate the average price.

  • Step 1: Define the Binance WebSocket URL and constants to use in the project.

      const (
          binanceWebSocketEndpoint = "wss://stream.binance.com:9443/ws/btcusdt@depth"
          responseStreamName       = "depthUpdate"
      )
    
  • Step 2: Create a function to connect to Binance WebSocket and handle any connection errors.

      func connectToBinance() *websocket.Conn {
          log.Printf("Connecting to Binance WebSocket at %s", binanceWebSocketEndpoint)
    
          conn, _, err := websocket.DefaultDialer.Dial(binanceWebSocketEndpoint, nil)
          if err != nil {
              log.Fatal("Error connecting to WebSocket:", err)
          } else {
              log.Println("Connected to Binance WebSocket successfully.")
          }
    
          return conn
      }
    

    This function uses the websocket.DefaultDialer.Dial() method to establish the WebSocket connection with Binance.


3. Processing Binance Order Book Data and Calculating the Average Price

Once connected to Binance, we need to process the incoming WebSocket messages, parse the order book data, and calculate the average price of bids and asks.

  • Step 1: Define a struct to map Binance’s WebSocket message data. The OrderBook struct will hold bid and ask data.

      type OrderBook struct {
          LastUpdateID int64      `json:"u"`
          Bids         [][]string `json:"b"` // [["price", "quantity"]]
          Asks         [][]string `json:"a"` // [["price", "quantity"]]
      }
    
  • Step 2: Create a function to handle messages from the WebSocket. This function listens for incoming messages, checks if they belong to the desired data stream, and calculates the average price from the order book.

      func processMessage(conn *websocket.Conn) {
          defer conn.Close()
    
          for {
              _, message, err := conn.ReadMessage()
              if err != nil {
                  log.Println("Error reading message:", err)
                  return
              }
    
              var data map[string]interface{}
              json.Unmarshal(message, &data)
    
              if stream, ok := data["e"].(string); ok && stream == responseStreamName {
                  var orderBook OrderBook
                  json.Unmarshal(message, &orderBook)
    
                  averagePrice := calculateAverage(orderBook)
                  log.Printf("Average Price: %.2f\n", averagePrice)
    
                  broadcast <- averagePrice
              }
          }
      }
    
  • Step 3: Implement the function that calculates the average price from the bids and asks.

      func calculateAverage(orderBook OrderBook) float64 {
          var totalPrice float64
          var totalCount int
    
          for _, bid := range orderBook.Bids {
              price, _ := strconv.ParseFloat(bid[0], 64)
              totalPrice += price
              totalCount++
          }
    
          for _, ask := range orderBook.Asks {
              price, _ := strconv.ParseFloat(ask[0], 64)
              totalPrice += price
              totalCount++
          }
    
          return totalPrice / float64(totalCount)
      }
    

This function sums up all the bid and ask prices and divides the total by the number of entries to get the average price.


4. WebSocket to Broadcast Data

Finally, you’ll need to set up a WebSocket server in Go to broadcast the calculated average price to connected frontend clients.

  • Step 1: Set up a handler for incoming WebSocket connections. This will allow multiple clients to connect and receive real-time price updates.

      func handleConnections(w http.ResponseWriter, r *http.Request) {
          ws, err := upgrader.Upgrade(w, r, nil)
          if err != nil {
              log.Fatal("Error upgrading to WebSocket:", err)
          }
          defer ws.Close()
    
          clients.Store(ws, true)
    
          for {
              _, _, err := ws.ReadMessage()
              if err != nil {
                  clients.Delete(ws)
                  break
              }
          }
      }
    
  • Step 2: Implement the function to broadcast the average price to all connected clients.

      func handleMessages() {
          for {
              avgPrice := <-broadcast
              clients.Range(func(client, _ interface{}) bool {
                  ws := client.(*websocket.Conn)
                  err := ws.WriteJSON(avgPrice)
                  if err != nil {
                      log.Printf("Error writing message to client: %v", err)
                      ws.Close()
                      clients.Delete(client)
                  }
                  return true
              })
          }
      }
    
  • Step 3: Start the WebSocket server in the main() function, and run the message processing and broadcasting concurrently.

      func main() {
          conn := connectToBinance()
    
          go processMessage(conn)
          go handleMessages()
    
          http.HandleFunc("/ws", handleConnections)
          log.Println("WebSocket server started on :8080")
          log.Fatal(http.ListenAndServe(":8080", nil))
      }
    

Now, the backend is fully set up to connect to Binance WebSocket, calculate the average price of bids and asks, and broadcast the results to connected WebSocket clients.

Frontend: Step-by-Step Guide

In this section, we will set up the frontend using Svelte, which will connect to the backend WebSocket server to receive real-time price updates and display them in a table. We’ll start from setting up the project, move on to connecting the frontend to the WebSocket, and finally, implement a simple UI to display the price data.


1. Setting up the Frontend Project

To begin, we will create a new Svelte project using Vite.

  1. Create the project with Vite: Run the following command to create a new project using Vite.

     npm create vite@latest
    

    Select Svelte when prompted for a framework. Name the project (e.g., crypto-price-stream-frontend).

  2. Install dependencies: After the project is created, navigate to the project directory and install the dependencies.

     cd crypto-price-stream-frontend
     npm install
    
  3. Run the development server: Start the development server to make sure everything is working.

     npm run dev
    

You should now have a working Svelte project.


2. Setting up WebSocket Connection

Next, we will modify the App.svelte file to connect to the backend WebSocket server, receive real-time average price data, and display it.

  • Step 1: Open src/App.svelte and replace the content with the following code to connect to the WebSocket and manage the state of price updates:

      <script lang="ts">
        import { onMount, onDestroy } from "svelte";
    
        let connectionStatus: string = "Connecting...";
        let ws: WebSocket | null = null;
        let reconnectInterval: number | null = null;
        let prices: { price: number; time: string }[] = []; // array to store prices with time
    
        // function to create WebSocket connection
        function connect() {
          ws = new WebSocket("ws://localhost:8080/ws");
    
          ws.onmessage = (event) => {
            const data = JSON.parse(event.data);
            addPrice(data); // adding price to prices array
          };
    
          ws.onerror = (error) => {
            console.error("WebSocket Error:", error);
            connectionStatus = "Connection error"; // updating connection status
          };
    
          ws.onclose = () => {
            connectionStatus = "Connection Lost, reconnecting..."; // updating connection status
            attemptReconnect(); // attempting reconnection
          };
    
          connectionStatus = "Connected"; // setting initial connection status
        }
    
        // function to attempt reconnection
        function attemptReconnect() {
          if (reconnectInterval) return; // return if already reconnecting
    
          reconnectInterval = setInterval(() => {
            console.log("Attempting to reconnect...");
            connect(); // reconnecting
          }, 5000); // reconnecting every 5 seconds
        }
    
        // function to add price to prices array
        function addPrice(price: number) {
          if (prices.length >= 10) {
            prices.pop(); // remove oldest price
          }
          const time = new Date().toLocaleTimeString();
          prices = [{ price, time }, ...prices]; // add new price with time
        }
    
        onMount(() => {
          connect(); // creating initial connection
        });
    
        onDestroy(() => {
          if (ws) {
            ws.close(); // close WebSocket connection on unmount
          }
    
          if (reconnectInterval) {
            clearInterval(reconnectInterval); // clear reconnect interval
          }
        });
      </script>
    
      <main>
        <h1>Average Prices</h1>
        <p>Connection status: {connectionStatus}</p>
    
        {#if prices.length > 0}
          <!-- table to show prices with time -->
          <table>
            <thead>
              <tr>
                <th>Price</th>
                <th>Time</th>
              </tr>
            </thead>
            <tbody>
              {#each prices as { price, time }}
                <tr>
                  <td>{price.toFixed(2)}</td>
                  <td>{time}</td>
                </tr>
              {/each}
            </tbody>
          </table>
        {:else}
          <p>Loading...</p>
        {/if}
      </main>
    
      <style>
        main {
          text-align: center;
          padding: 1em;
          margin: 0 auto;
        }
    
        h1 {
          color: #c6ac8f;
        }
    
        p {
          font-size: 1.2em;
        }
    
        table {
          width: 100%;
          border-collapse: collapse;
          margin-top: 1em;
        }
    
        th,
        td {
          padding: 8px;
          border: 1px solid #c6ac8f;
          text-align: center;
        }
    
        th {
          background-color: #5e503f;
        }
      </style>
    
  • Step 2: Here’s what’s happening in the code:

    • The connect() function creates a WebSocket connection to the backend.

    • WebSocket messages are received, parsed, and added to the prices array.

    • If the WebSocket connection fails, it automatically tries to reconnect every 5 seconds.

    • The price data is displayed in a table, with the latest 10 prices.


3. Running the Frontend

  • Step 1: Make sure the backend WebSocket server is running on port 8080. You can check the backend setup from the previous section.

  • Step 2: In your terminal, start the frontend development server.

      npm run dev
    
  • Step 3: Visit http://localhost:5173 in your browser (or the URL provided by Vite), and you should see the average prices table being updated in real time.

You can find the full source code for both the backend and frontend of this project on GitHub. The repository includes all the steps we've covered in this blog:

GitHub Repository Link

Feel free to explore the code, clone the repository, and modify it as you like!


Conclusion

In this project, we created a real-time WebSocket service that streams average prices from the Binance order book and broadcasts them to connected clients. We set up the backend using Golang to handle WebSocket connections and perform price calculations, and we built a simple Svelte frontend to display these prices to users in real time.

0
Subscribe to my newsletter

Read articles from Rabinson Thapa directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Rabinson Thapa
Rabinson Thapa