top of page

Building a Real-Time Forex Dashboard with Streamlit and WebSocket

Nikhil Adithyan

A hands-on Python project



One of the traders’ main concerns regarding Forex trading is the cost of the transaction. However, cost is not only the fee your broker will charge you. The cost, and maybe the most important one, is the spread. The definition states that the bid-ask spread is the difference between the highest price a buyer is bidding (willing to pay) and the lowest price a seller is asking (willing to sell) for a currency pair. In practice, if you buy and sell the same amount, at the same time, for the same pair, the spread will make the buy more expensive and the sell cheaper.


In this article, we will create a dashboard that will continuously be updated with the bid and ask prices for various forex pairs while at the same time keeping and displaying the highest and the lowest of them.


Tracking the highest and lowest bid-ask prices is crucial to identify price ranges, potential breakouts, and market volatility. This information helps set stop-loss orders and recognize support and resistance levels.


Challenges in Implementation and Why a WebSocket?

In theory, we could get into a loop and continuously show the information, but this has more downs than upsides.


  • hit hard on server resources and potentially trigger rate-limiting

  • network overhead due to repeated HTTP handshakes and headers.

  • frequent requests will result in skipped price updates.


WebSockets address these issues by maintaining a persistent, low-latency connection that allows instant data to be pushed from the server. This reduces network traffic and server load and ensures more timely updates.


To implement this small project, for our data, we will use TraderMade and especially their WebSocket product which will provide us the prices in real time. For displaying, we are going to use Streamlit.


Understanding WebSockets

First, I’ll explain step by step how a WebSocket works. Python provides a simple but effective web socket library that will be used called (apparently) “websocket”. Let’s import it.



import websocket

The first thing we should do with a WebSocket is to define the connection. This will create a function to be used to establish the connection.



def run_websocket():
    ws = websocket.WebSocketApp(
        "wss://marketdata.tradermade.com/feedadv",
        on_message=on_message,
        on_error=on_error,
        on_close=on_close
    )
    ws.on_open = on_open
    ws.run_forever()
    print("WebSocket thread started")

After defining the WebSocket app (ws) with the link (wss), we should define what the code should do in case you receive the prices (on_message), what if there is an error from the server (on_error) and what should be done when the connection is closed.


The on_open function practically opens the streaming of the WebSocket, and then the run_forever initiates the whole process.

Let’s define one by one and what each one will do practically:



def on_open(ws):
    ws.send('{"userKey": "<YOUR_KEY>", "symbol":"EURUSD,GBPUSD,AUDUSD,NZDUSD,USDJPY,USDCHF,USDCAD,USDNOK,USDSEK"}')

The on_open will provide the key to the servers and the symbols (pairs) for which we would like to start receiving information. This is called only once in the beginning.



def on_error(ws, error):
    print(f"WebSocket Error: {error}")

def on_close(ws, close_status_code, close_msg):
    print("WebSocket connection closed")

For the on_error and on_close, it is self-explanatory when these are triggered. In our code, we will just print a message — however, in a real-life project, you may trigger important decisions to buy or sell, and you might want to define there to close your open positions for obvious reasons.



def on_message(ws, message):
    print(message)

The on_message is the core of a WebSocket since this function will be called each time you receive a message from the web server.


If you run the above code, you will receive messages (if the market is open) of the bid and ask prices for the pairs defined.



For TraderMade, each message is in JSON format and is for one pair-one event


Let’s Build our Dashboard

Enough now with theory. In order to build the dashboard using streamlit, you should create a streamlit app.


Please note that the code in the .py file should be in the below order.


Let’s do the imports first.



import streamlit as st
import websocket
import threading
import queue
from streamlit.runtime.scriptrunner import add_script_run_ctx
import pandas as pd
import json
from datetime import datetime, timezone

Set the layout of your dashboard to wide so it gets the full size of the browser.



st.set_page_config(layout="wide")

Then, we initiate the session state variables, which will maintain and share data across different runs of a Streamlit application for each user session.


We will keep a queue where we will store the messages from the WebSocket and also define a thread where we can maintain long-running processes across multiple reruns of our app.



# Initialize session state variables
if "message_queue" not in st.session_state:
    st.session_state.message_queue = queue.Queue(maxsize=1000)

if "websocket_thread" not in st.session_state:
    print("WebSocket thread not found")
    st.session_state.websocket_thread = None

Now we are going to define a dataframe (which will run only once) that is going to be the dataframe displayed from our app.



if "dataframe" not in st.session_state:
    utc_now = datetime.now(timezone.utc)
    st.session_state.dataframe = [
        {"symbol": "EURUSD", "time": utc_now, "bid": "000000.0000", "ask": "000000.0000", "mid": "000000.0000", "spread": 0, "h_spread" : 0, "l_spread" : 99999999 },
        {"symbol": "GBPUSD", "time": utc_now, "bid": "000000.0000", "ask": "000000.0000", "mid": "000000.0000", "spread": 0, "h_spread" : 0, "l_spread" : 99999999 },
        {"symbol": "NZDUSD", "time": utc_now, "bid": "000000.0000", "ask": "000000.0000", "mid": "000000.0000", "spread": 0, "h_spread" : 0, "l_spread" : 99999999 },
        {"symbol": "USDJPY", "time": utc_now, "bid": "000000.0000", "ask": "000000.0000", "mid": "000000.0000", "spread": 0, "h_spread" : 0, "l_spread" : 99999999 },
        {"symbol": "USDCHF", "time": utc_now, "bid": "000000.0000", "ask": "000000.0000", "mid": "000000.0000", "spread": 0, "h_spread" : 0, "l_spread" : 99999999 },
        {"symbol": "AUDUSD", "time": utc_now, "bid": "000000.0000", "ask": "000000.0000", "mid": "000000.0000", "spread": 0, "h_spread" : 0, "l_spread" : 99999999 },
        {"symbol": "USDCAD", "time": utc_now, "bid": "000000.0000", "ask": "000000.0000", "mid": "000000.0000", "spread": 0, "h_spread" : 0, "l_spread" : 99999999 },
        {"symbol": "USDNOK", "time": utc_now, "bid": "000000.0000", "ask": "000000.0000", "mid": "000000.0000", "spread": 0, "h_spread" : 0, "l_spread" : 99999999 },
        {"symbol": "USDSEK", "time": utc_now, "bid": "000000.0000", "ask": "000000.0000", "mid": "000000.0000", "spread": 0, "h_spread" : 0, "l_spread" : 99999999 },
]

It is time to give a title to our app and also put a placeholder to display the dataframe defined above.



# Streamlit UI setup
st.title("Real-Time WebSocket Client")
# placeholder = st.empty()  # Placeholder for displaying messages
df_placeholder = st.dataframe(pd.DataFrame(st.session_state.dataframe))

Below are the updated WebSocket functions that we discussed before. Practically, the only difference is that we will add the message to the queue.



def on_message(ws, message):
    st.session_state.message_queue.put(message)

def on_error(ws, error):
    print(f"WebSocket Error: {error}")

def on_close(ws, close_status_code, close_msg):
    print("WebSocket connection closed")

def on_open(ws):
    ws.send('{"userKey": "<YOUR_KEY>", "symbol":"EURUSD,GBPUSD,AUDUSD,NZDUSD,USDJPY,USDCHF,USDCAD,USDNOK,USDSEK"}')

def run_websocket():
    ws = websocket.WebSocketApp(
        "wss://marketdata.tradermade.com/feedadv",
        on_message=on_message,
        on_error=on_error,
        on_close=on_close
    )
    ws.on_open = on_open
    ws.run_forever()
    print("WebSocket thread started")

The below function can be considered the core of our dashboard. It will be called for each message received (from the queue) and update the dataframe displayed.



def get_list_to_update_session_state(json_message_l, list_of_pairs):
    for index, value in enumerate(list_of_pairs):
        if value["symbol"] == json_message_l["symbol"]:
            epoch_seconds = int(json_message_l["ts"]) / 1000
            datetime_str = datetime.fromtimestamp(epoch_seconds, tz=timezone.utc).strftime('%Y-%m-%d %H:%M:%S')
            # print("index ----->", index)
            
            list_of_pairs[index]["time"] = datetime_str
            list_of_pairs[index]["bid"] = json_message_l["bid"]
            list_of_pairs[index]["ask"] = json_message_l["ask"]
            list_of_pairs[index]["mid"] = json_message_l["mid"]
            
            spread = float(json_message_l["ask"]) - float(json_message_l["bid"])
            
            if "JPY" in value["symbol"] or "NOK" in value["symbol"] or "SEK" in value["symbol"]:
                spread = round(spread * 100, 2)
            else:
                spread = round(spread * 10000, 2)
            
            list_of_pairs[index]["spread"] = str(spread) + ' pips'
            
            if list_of_pairs[index]["h_spread"] < spread:
                list_of_pairs[index]["h_spread"] = spread
            if list_of_pairs[index]["l_spread"] > spread:
                list_of_pairs[index]["l_spread"] = spread

            break
    
    return list_of_pairs

Now, it is time to run the WebSocket. By checking if the thread exists, we ensure that the websocket initialization of the connection will only run once in the beginning.



if st.session_state.websocket_thread is None or not st.session_state.websocket_thread.is_alive():
    print("Starting WebSocket connection...")
    websocket_thread = threading.Thread(target=run_websocket, daemon=True)
    add_script_run_ctx(websocket_thread)
    websocket_thread.start()
    st.session_state.websocket_thread = websocket_thread

Now is the time for the game! We will get into a constant loop (while true), and inside the loop, we will get the unprocessed messages and update the dataframe we use to display the information.



while True:
    while not st.session_state.message_queue.empty():
        message = st.session_state.message_queue.get()
        try:
            json_message = json.loads(message)
            st.session_state.dataframe = get_list_to_update_session_state(json_message,st.session_state.dataframe)
            df_placeholder.dataframe(st.session_state.dataframe,
                                     # {"symbol": "EURUSD", "time": utc_now, "bid": "000000.0000", "ask": "000000.0000",
                                     #  "mid": "000000.0000", "spread": 0, "Highest Spread": 0, "Lowest Spread": 99999999}
                column_config={
                    "symbol": st.column_config.TextColumn(label="Symbol", width="small"),
                    "time": st.column_config.TextColumn(label="Time", width="medium"),
                    "bid": st.column_config.NumberColumn(label="Bid", width="small"),
                    "ask": st.column_config.NumberColumn(label="Ask", width="small"),
                    "mid": st.column_config.NumberColumn(label="Mid", width="small"),
                    "spread": st.column_config.TextColumn(label="Spread", width="small"),
                    "h_spread": st.column_config.NumberColumn(label="High", width="small"),
                    "l_spread": st.column_config.NumberColumn(label="Low", width="small"),
                },
                use_container_width=True)
        except Exception as e:
            print(e)
            pass

The files should be saved in any name, but the conventional way is as app.py


From the terminal, you should go to the folder that you have saved the file and run.


streamlit run .\app.py

Voila! Your browser will open, and you will see that the list of pairs is continuously updated with the latest information.



Play with it

Some ideas for you to enhance further the code and understand WebSockets while at the same time investigating how this can work for you are listed below:


  • Send notifications when the current spread difference is significantly lower or higher than the average one


  • Get the candles of the pairs in a dataframe, and then, using the messages update them so you have constantly updated pricing without the need to call some other service for the prices.


  • Implement a signal based on process and spreads — For example, you implement your signaling method but at the same time, you filter (cancel) the signal if the spreads are in the high area.


  • Put some more print statements (or logs) so you can follow through the execution feasibly and understand Streamlit and WebSockets in one go.


And much more. The sky is the limit with this architecture.


Conclusion

In this article, we tried to explore the power of combining WebSockets and Streamlit to create a real-time Forex dashboard. By leveraging TraderMade’s WebSocket feed, we could monitor bid-ask spreads and price movements across multiple currency pairs in one screen. This approach overcomes the limitations of continuous API requests, providing a flexible foundation for advanced trading tools and analysis.


I highly encourage you to look into and try implementing the further improvements that we discussed before. You will not only gain more hands-on experience but it will elevate this project to the next level.


With that being said, you’ve reached the end of the article. We hope you enjoyed the article. Stay tuned for more!


The entire code used in this article:



import streamlit as st
import websocket
import threading
import queue
from streamlit.runtime.scriptrunner import add_script_run_ctx
import pandas as pd
import json
from datetime import datetime, timezone

st.set_page_config(layout="wide")

# Initialize session state variables
if "message_queue" not in st.session_state:
    st.session_state.message_queue = queue.Queue(maxsize=1000)

if "websocket_thread" not in st.session_state:
    print("WebSocket thread not found")
    st.session_state.websocket_thread = None

if "dataframe" not in st.session_state:
    utc_now = datetime.now(timezone.utc)
    st.session_state.dataframe = [
        {"symbol": "EURUSD", "time": utc_now, "bid": "000000.0000", "ask": "000000.0000", "mid": "000000.0000", "spread": 0, "h_spread" : 0, "l_spread" : 99999999 },
        {"symbol": "GBPUSD", "time": utc_now, "bid": "000000.0000", "ask": "000000.0000", "mid": "000000.0000", "spread": 0, "h_spread" : 0, "l_spread" : 99999999 },
        {"symbol": "NZDUSD", "time": utc_now, "bid": "000000.0000", "ask": "000000.0000", "mid": "000000.0000", "spread": 0, "h_spread" : 0, "l_spread" : 99999999 },
        {"symbol": "USDJPY", "time": utc_now, "bid": "000000.0000", "ask": "000000.0000", "mid": "000000.0000", "spread": 0, "h_spread" : 0, "l_spread" : 99999999 },
        {"symbol": "USDCHF", "time": utc_now, "bid": "000000.0000", "ask": "000000.0000", "mid": "000000.0000", "spread": 0, "h_spread" : 0, "l_spread" : 99999999 },
        {"symbol": "AUDUSD", "time": utc_now, "bid": "000000.0000", "ask": "000000.0000", "mid": "000000.0000", "spread": 0, "h_spread" : 0, "l_spread" : 99999999 },
        {"symbol": "USDCAD", "time": utc_now, "bid": "000000.0000", "ask": "000000.0000", "mid": "000000.0000", "spread": 0, "h_spread" : 0, "l_spread" : 99999999 },
        {"symbol": "USDNOK", "time": utc_now, "bid": "000000.0000", "ask": "000000.0000", "mid": "000000.0000", "spread": 0, "h_spread" : 0, "l_spread" : 99999999 },
        {"symbol": "USDSEK", "time": utc_now, "bid": "000000.0000", "ask": "000000.0000", "mid": "000000.0000", "spread": 0, "h_spread" : 0, "l_spread" : 99999999 },
    ]

# Streamlit UI setup
st.title("Real-Time WebSocket Client")
# placeholder = st.empty()  # Placeholder for displaying messages
df_placeholder = st.dataframe(pd.DataFrame(st.session_state.dataframe))

def on_message(ws, message):
    # if "message_queue" in st.session_state:
    st.session_state.message_queue.put(message)
    print(st.session_state.message_queue.qsize())
    # print(message)

def on_error(ws, error):
    print(f"WebSocket Error: {error}")

def on_close(ws, close_status_code, close_msg):
    print("WebSocket connection closed")

def on_open(ws):
    ws.send('{"userKey": "YOUR API KEY", "symbol":"EURUSD,GBPUSD,AUDUSD,NZDUSD,USDJPY,USDCHF,USDCAD,USDNOK,USDSEK"}')

def run_websocket():
    ws = websocket.WebSocketApp(
        "wss://marketdata.tradermade.com/feedadv",
        on_message=on_message,
        on_error=on_error,
        on_close=on_close
    )
    ws.on_open = on_open
    ws.run_forever()
    print("WebSocket thread started")

def get_list_to_update_session_state(json_message_l, list_of_pairs):
    for index, value in enumerate(list_of_pairs):
        if value["symbol"] == json_message_l["symbol"]:
            epoch_seconds = int(json_message_l["ts"]) / 1000
            datetime_str = datetime.fromtimestamp(epoch_seconds, tz=timezone.utc).strftime('%Y-%m-%d %H:%M:%S')
            # print("index ----->", index)
            
            list_of_pairs[index]["time"] = datetime_str
            list_of_pairs[index]["bid"] = json_message_l["bid"]
            list_of_pairs[index]["ask"] = json_message_l["ask"]
            list_of_pairs[index]["mid"] = json_message_l["mid"]
            
            spread = float(json_message_l["ask"]) - float(json_message_l["bid"])
            
            if "JPY" in value["symbol"] or "NOK" in value["symbol"] or "SEK" in value["symbol"]:
                spread = round(spread * 100, 2)
            else:
                spread = round(spread * 10000, 2)
            
            list_of_pairs[index]["spread"] = str(spread) + ' pips'
            
            if list_of_pairs[index]["h_spread"] < spread:
                list_of_pairs[index]["h_spread"] = spread
            if list_of_pairs[index]["l_spread"] > spread:
                list_of_pairs[index]["l_spread"] = spread

            break
            
    return list_of_pairs

if st.session_state.websocket_thread is None or not st.session_state.websocket_thread.is_alive():
    print("Starting WebSocket connection...")
    websocket_thread = threading.Thread(target=run_websocket, daemon=True)
    add_script_run_ctx(websocket_thread)
    websocket_thread.start()
    st.session_state.websocket_thread = websocket_thread

# Process messages from the queue and update UI
if st.session_state.message_queue.empty():
    # placeholder.write("Waiting for messages...")
    print("Waiting for messages...")
else:
    pass

while True:
    while not st.session_state.message_queue.empty():
        message = st.session_state.message_queue.get()
        # placeholder.write(message)
        # print(message)
        try:
            json_message = json.loads(message)
            st.session_state.dataframe = get_list_to_update_session_state(json_message,st.session_state.dataframe)
            df_placeholder.dataframe(st.session_state.dataframe,
                                     # {"symbol": "EURUSD", "time": utc_now, "bid": "000000.0000", "ask": "000000.0000",
                                     #  "mid": "000000.0000", "spread": 0, "Highest Spread": 0, "Lowest Spread": 99999999}
                column_config={
                    "symbol": st.column_config.TextColumn(label="Symbol", width="small"),
                    "time": st.column_config.TextColumn(label="Time", width="medium"),
                    "bid": st.column_config.NumberColumn(label="Bid", width="small"),
                    "ask": st.column_config.NumberColumn(label="Ask", width="small"),
                    "mid": st.column_config.NumberColumn(label="Mid", width="small"),
                    "spread": st.column_config.TextColumn(label="Spread", width="small"),
                    "h_spread": st.column_config.NumberColumn(label="High", width="small"),
                    "l_spread": st.column_config.NumberColumn(label="Low", width="small"),
                },
                use_container_width=True)  # Optional: Makes the table fill the container width)
        except Exception as e:
            print(e)
            pass

Comentarios


Bring information-rich articles and research works straight to your inbox (it's not that hard). 

Thanks for subscribing!

© 2023 by InsightBig. Powered and secured by Wix

bottom of page