I have always been fascinated by the visualization of real-time data. Even at slightly over 50, I am still amazed by how this technology has transformed how we understand data. The most exciting is the ability to analyze information as it happens and make decisions based on that data. This is one of the reasons why I like trading. Well, to be more precise, I like analysing trading data.
Having that in mind, I was excited to explore how we can display trades as they happen, not the conventional way with OHLC candles but individually. What better way to add the volume parameter to the chart than through a well-known indicator named VWAP?
What you should expect from this article
- I will use the WebSocket of EODHD to stream the BTC-USD trades as they happen.
- I will calculate the VWAP on those trades
- I will use a Dash app to plot the information live
Note: This article is more about displaying the technology than discussing the outcome. But I genuinely believe that if you go to the end of the article, you will find that it will trigger your own ideas, and you will find yourself testing new approaches and visualizations.
Let’s start coding
First, we will do our imports
import dash
from dash import dcc, html, Input, Output, State, callback, ctx
from dash_extensions import WebSocket
import plotly.graph_objects as go
import pandas as pd
import json
Now, we should define the Dash app and set some parameters to be used later in our code
app = dash.Dash(__name__)
API_KEY = '<YOUR_EODHD_API_KEY>'
KEEP_LAST_N_TRADES = 5000 # How many trades will be displayed
ROLLING_VWAP_WINDOW_SIZE = 100 # the periods that the rolling vwap will be calculated
STANDARD_DEVIATION_MULTIPLIER = 1 # STD used for upper and lower band
Before we move further, let me give you a first look at the chart so the above parameters make sense.

The trades will be displayed as they happen. The blue line is the trade price, the yellow is the VWAP, while the green and red lines are the upper and lower channels. If you want to learn more about VWAP, I wrote an article recently. Just click the below:
The Websocket
For the less technical readers, let me explain what a WebSocket is. WebSockets act like a live phone line between apps and servers, letting them chat instantly without constant refreshing.
For our case, we will use the EODHD websocket. In addition to the crypto we will use in this article, they have web sockets for Forex and Stocks. If you have any idea about a Stock dashboard, comment here. I will be happy to try something out.
ws_url = f"wss://ws.eodhistoricaldata.com/ws/crypto?api_token={API_KEY}"
app.layout = html.Div([
dcc.Graph(id='live-graph', style={'height': '90vh'}),
html.Div(id='status'),
WebSocket(id="ws",
url=ws_url,
state={"message": None}),
dcc.Store(id='store', data=[])
])
@callback(
Output("ws", "send"),
Input("ws", "state")
)
def subscribe(state):
if ctx.triggered_id == "ws": # Only send when WebSocket state changes
print("Attempting to subscribe...")
return json.dumps({
"action": "subscribe",
"symbols": "BTC-USD"
})
return dash.no_update
So, the code does the following:
- First, you need to set up the URL of the WebSocket. As you will notice, the WebSocket starts with WSS and not HTTPS.
- Then, we set the Dash application layout. This is where we will set up how our dashboard will look.
- Finally, we use a callback function to subscribe for BTC/USD trades (this is called whenever the state of the socket changes, so apparently will also be send in the initiation)
Processing of data
Now, we should create a function where the message from the WebSocket will be sent.
The message is like:
{"s":"BTC-USD","p":"83822.0465","q":"0.07913843","dc":"-0.7097","dd":"-594.8615","t":1743233138401}
So our function will be like:
def process_trade_data(data):
# Convert the list of dictionaries to a DataFrame
df = pd.DataFrame(data)
if not df.empty:
# Calculate rolling VWAP with window of 50
df['price_volume'] = df['price'] * df['quantity']
df['rolling_price_volume'] = df['price_volume'].rolling(window=ROLLING_VWAP_WINDOW_SIZE).sum()
df['rolling_volume'] = df['quantity'].rolling(window=ROLLING_VWAP_WINDOW_SIZE).sum()
df['vwap'] = df['rolling_price_volume'] / df['rolling_volume']
df['rolling_std'] = df['vwap'].rolling(window=ROLLING_VWAP_WINDOW_SIZE).std()
df['vwap_upper_channel'] = df['vwap'] + (df['rolling_std'] * STANDARD_DEVIATION_MULTIPLIER)
df['vwap_lower_channel'] = df['vwap'] - (df['rolling_std'] * STANDARD_DEVIATION_MULTIPLIER)
# Drop intermediate calculation columns
df = df.drop(['price_volume', 'rolling_price_volume', 'rolling_volume'], axis=1)
# Convert back to list of dictionaries
processed_data = df.to_dict('records')
return processed_data[-1 * KEEP_LAST_N_TRADES:] # Keep last 1000 trades
return data
Practically, this function gets the data (where later we will store the messages as they come in), converts them to a dataframe, calculates the VWAP, and sends it back again as a list of dictionaries.
But all happens in the following function:
@callback(
Output("store", "data"),
Input("ws", "message"),
State("store", "data"),
prevent_initial_call=True
)
def update_store(message, data):
try:
trade = json.loads(message["data"])
new_entry = {
'price': float(trade['p']),
'quantity': float(trade['q']),
'timestamp': pd.to_datetime(trade['t'], unit='ms')
}
# Add new entry to data
updated_data = data[-1 * KEEP_LAST_N_TRADES:] + [new_entry]
# Process the data through the new function
processed_data = process_trade_data(updated_data)
return processed_data
except Exception as e:
print("Error >>>>> " , e)
return dash.no_update
This function is called every time a message comes in from the WebSocket. We will keep only the price, quantity, and timestamp and add them to the list “data,” where we keep all the messages.
You will notice that we keep the last trades as defined in the parameter KEEP_LAST_N_TRADES, so we don’t reach a point where we have a million entries and crash our system 😉
Now, we will set up our Dash graph:
@callback(
Output('live-graph', 'figure'),
Input('store', 'data'),
prevent_initial_call=True
)
def update_graph(data):
df = pd.DataFrame(data)
fig = go.Figure()
if not df.empty:
fig.add_trace(go.Scatter(
x=df['timestamp'],
y=df['price'],
mode='lines',
line=dict(
width=2,
color='cyan'
),
name='Trades'
))
fig.add_trace(go.Scatter(
x=df['timestamp'],
y=df['vwap'],
mode='lines',
line=dict(
width=2,
color='yellow'
),
name='VWAP'
))
fig.add_trace(go.Scatter(
x=df['timestamp'],
y=df['vwap_upper_channel'],
mode='lines',
line=dict(
width=1,
color='green'
),
name='VWAP upper channel'
))
fig.add_trace(go.Scatter(
x=df['timestamp'],
y=df['vwap_lower_channel'],
mode='lines',
line=dict(
width=1,
color='red'
),
name='VWAP lower channel'
))
fig.update_layout(
title='BTC/USD Real-Time Trades',
xaxis_title='Time',
yaxis_title='Price',
template='plotly_dark',
hovermode='x unified'
)
return fig
We are plotting 4 lines:
- Price of the trade
- The VWAP
- and the upper and lower bands of the VWAP
One more function, which is more like a technicality
@callback(
Output("status", "children"),
Input("ws", "error")
)
def show_error(error):
if error:
return f"Connection error: {error['message']}"
return "Connected successfully"
This will show if we have any errors during the connection on our dashboard under the chart.
Last, we run our app
if __name__ == '__main__':
app.run(debug=True)
If you save all the code in one .py file and we run it, you should have the graph up and running, updating constantly!

Key Takeaways:
- Real-time Data Visualization: Utilize Dash for interactive dashboards.
- WebSockets: Stream live trade data efficiently with EODHD
- VWAP Indicator: Calculate and plot Volume Weighted Average Price dynamically.