Back to Tutorials
Real-Time Market Quotes with SocketIO + WebSockets + JavaScript - Part 1

Real-Time Market Quotes with SocketIO + WebSockets + JavaScript - Part 1

Making a real-time Market quotes board is a smart way to monitor Live market rates. You won't need to make constant API requests. This guide will show you how to build a Quotes board for FX, UKOIL, XAUUSD, Crypto, and the UK100 index. We will use HTML, JavaScript, Python, WebSockets, and SocketIO to get live milliseconds updates. This setup allows for live data streaming to monitor live bid-ask rates.

This tutorial is part one of a two-part series. In it, we will start a Flask app wrapped in SocketIO and connected to a WebSocket to stream real-time rates to the frontend. The Frontend will parse rates and display real-time bid-ask prices for over 10+ symbols.

Part two will add greater frontend functionality, including live tick charts, spreads, connect-disconnect functions, and flashing rates.

Let's begin!

Before we install dependencies, let's look at the file structure:

/templates
├── index.html
app.py
.env
requirements.txt

Backend

Setup

We will first start with the backend and install dependencies.

pip install python-dotenv flask flask-socketio websockets

Now we create our .env file in the root directory containing the following:

TRADERMADE_API_KEY=your_websocket_api_key_here

You need to add your WebSocket API Key. Then we create an app.py file and import libraries:

Importing Libraries

import asyncio
import json
import websockets
import threading
import time
import os
from flask import Flask, render_template, send_from_directory
from flask_socketio import SocketIO
from dotenv import load_dotenv

Next, we will load our env file to set our websocket key later.

Loading the .env File

from dotenv import load_dotenv
load_dotenv()

Now that we have our setup, we will start writing some code. We will create a Flask web app with a static folder exposed for CSS and JavaScript files. Then, we will add socketio, which wraps the Flask app and emits events to the frontend.

Setting Up the Flask SocketIO Server

app = Flask(__name__, static_folder='static')
socketio = SocketIO(app, cors_allowed_origins="*")

We will declare latest_data in a dictionary for storage and add a list of symbols we need to request and display data. This list can be dynamic and changed from the frontend, but we have added it to the backend for simplicity.

latest_data = {}  
symbols = [  
'EURUSD', 'USDJPY', 'GBPUSD', 'US30USD',   
'USDCAD', 'USDCHF', 'AUDUSD', 'OILUSD',   
'EURCHF', 'EURJPY', 'UKOILUSD', 'UK100USD',   
'XAUUSD', 'BTCUSD', 'EURNOK'  
]

Next, we will define the streaming class connecting to the data source and hold configuration: API key, symbols list, connect and reconnect logic, and a running flag.

Defining the WebSocket Class

class TraderMadeStream:
    def __init__(self):
        self.api_key = None
        self.symbols = []
        self.websocket = None
        self.reconnect_attempts = 0
        self.max_reconnect_attempts = 5
        self.reconnect_delay = 3  # seconds
        self.running = False

Within the class, we define setter methods: set_ws_key and set_symbols handle our WebSocket key and symbols needed to request our data.

def set_ws_key(self, api_key):
  """Set the TraderMade WebSocket API key"""
  self.api_key = api_key

def set_symbols(self, symbols_string):
  """Set the symbols to stream, as a comma-separated string"""
  self.symbols = [s.strip() for s in symbols_string.split(',')] 

We now define connect_and_stream as an asynchronous method that establishes the WebSocket connection and handles the data streaming.

 async def connect_and_stream(self):
        """Connect to the TraderMade WebSocket and stream data"""
        if not self.api_key:
            raise ValueError("API Key not set. Use set_ws_key() first.")
        if not self.symbols:
            raise ValueError("No symbols set. Use set_symbols() first.")

        self.running = True
        while self.running:
            try:
                uri = f"wss://marketdata.tradermade.com/feedadv?api_key={self.api_key}"
                async with websockets.connect(uri) as websocket:
                    self.websocket = websocket
                    self.reconnect_attempts = 0
                    print("WebSocket connection established")

                    # Subscribe to the specified symbols
                    subscribe_msg = {
                        "userKey": self.api_key,
                        "symbol": ",".join(self.symbols)
                    }
                    await websocket.send(json.dumps(subscribe_msg))

                    # Process incoming messages
                    while self.running:
                        message = await websocket.recv()
                        # Skip if empty
                        if not message.strip():
                            continue
                        # Skip initial confirmation message "connected"
                        if message.strip().lower() == "connected":
                            print("Received initial connection confirmation, skipping message")
                            continue

                        try:
                            data = json.loads(message)
                        except json.JSONDecodeError as e:
                            print("Error decoding JSON message:", message, "-", e)
                            continue

                        self.process_message(data)

            except websockets.exceptions.ConnectionClosed:
                print("WebSocket connection closed")
                await self.handle_reconnect()
            except Exception as e:
                print(f"WebSocket error: {e}")
                await self.handle_reconnect()

We also define an asynchronous handle_reconnect method that manages reconnects, time between reconnects, and max reconnect attempts.

Adding Reconnection Logic

async def handle_reconnect(self):  
  """Handle reconnection logic"""  
  if not self.running: 
      return  
  if self.reconnect_attempts < self.max_reconnect_attempts:  
      self.reconnect_attempts += 1  
      reconnect_time = self.reconnect_delay * self.reconnect_attempts  
      print(f"Attempting to reconnect ({self.reconnect_attempts}/{self.max_reconnect_attempts}) in {reconnect_time} seconds...")  
      await asyncio.sleep(reconnect_time)  
  else:  
      print("Max reconnect attempts reached. Please check your connection or API key.")  
      self.running = False

The process_message method stores the latest data for the symbol in a global latest_data dictionary and emits the market update to connected clients using Socket.IO. The disconnect method just disconnects the WebSocket connection.

Adding Methods to Handle Data

def process_message(self, data):  
  """Handles incoming market data, stores it, and broadcasts it to connected clients."""  

  if isinstance(data, dict) and 'symbol' in data:  
      symbol = data['symbol']  
      latest_data[symbol] = data  
      print(data)  
      socketio.emit('market_update', data)

def disconnect(self):
        self.running = False
        print("WebSocket connection manually closed")

We further define one more method in the TraderMadeStream Class: The start_streaming method. This method is slightly complicated and crucial for our program to connect to the client without getting blocked.

This thread starts a separate thread and defines an inner function (run_streaming) that sets up the asyncio environment needed for WebSocket operations. The WebSocket connection and data streaming happen in this background thread, and the main application continues to run without waiting for the streaming to complete. When the main application exits, the daemon thread automatically terminates.

Setting Up the Streaming Thread

 def start_streaming(self):
        """Start the streaming in a separate thread"""
        def run_streaming():
            loop = asyncio.new_event_loop()
            asyncio.set_event_loop(loop)
            loop.run_until_complete(self.connect_and_stream())

        thread = threading.Thread(target=run_streaming)
        thread.daemon = True
        thread.start()

Once our class is defined, we create an instance to use for later.

tm_stream = TraderMadeStream()

Now that our WebSocket logic is defined, let's define routes and SocketIO events. The index.html is served on the home route, and socketIO events provide logic to connect and disconnect clientside, including emitting data on the connect event.

Defining Route and Events

@app.route('/')
def index():
    return render_template('index.html')

@socketio.on('connect')
def on_connect():
    tm_stream.start_streaming()
    if latest_data:
        socketio.emit('initial_data', latest_data)

@socketio.on('disconnect')
def on_disconnect():
    tm_stream.disconnect()

Finally, we pass our websocket key and symbols list to the tm_stream instance we defined earlier and start our Flask socketio server on port 5000. Our Backend is now complete.

Initiating The Server

if __name__ == "__main__":
    API_KEY = os.getenv("TRADERMADE_API_KEY")  
    if not API_KEY:
        print("Error: TRADERMADE_API_KEY not set")
        exit(1)

    tm_stream.set_ws_key(API_KEY)
    tm_stream.set_symbols(','.join(symbols))

    socketio.run(app, host='0.0.0.0', port=5000,  
                 debug=True, use_reloader=False)

Now that our backend is ready, we will show the lightning-fast rates in the browser.

Frontend

Adding the Real-Time Data Table

We will add a table within which our bid-ask will stream (in milliseconds).

<table id="rates">
  <thead><tr><th>Symbol</th><th>Bid</th><th>Ask</th></tr></thead>
  <tbody></tbody>
</table>

Setting up SocketIO on the Client Side

Now we will write a short JavaScript code to read rates and populate the body of the table. In the below code, we initiate SocketIO, select the rates id, and then insert:

<script src="https://cdn.socket.io/4.5.1/socket.io.min.js"></script>
<script>
  const socket = io();
  const tbody  = document.querySelector('#rates tbody');

  const upsert = ({ symbol, bid, ask }) => {
    if (!symbol) return;
    let row = document.getElementById(symbol) || (() => {
      const r = tbody.insertRow(); r.id = symbol;
      r.insertCell(); r.insertCell(); r.insertCell(); return r;
    })();
    [symbol, bid, ask].forEach((v,i)=> row.cells[i].textContent = v);
  };

Finally, we initiate the Socket and add any bulk data to the upsert, followed by the market_update event, where we upsert every live tick pushed by the backend.

/* initial snapshot (optional) */

socket.on('initial_data', snap =>   Object.values(snap).forEach(upsert));

/* stream updates */

socket.on('market_update', upsert)

The Final Act

We will now bring everything to life by running:

python app.py

Now open https://localhost:5000 and voila!

Demo Quotes Board

Get the full code from Github.

Related Tutorials