DEV Community

Cover image for Create a Market Profile Dashboard Using Python
Shridhar G Vatharkar
Shridhar G Vatharkar

Posted on

Create a Market Profile Dashboard Using Python

In this guide, you’ll learn how to build a Market Profile dashboard using Python. We’ll walk through how to use Streamlit, Plotly, and our Forex, CFD, and Crypto Data API to bring market data to life. Whether you're a trader, analyst, or developer, this tutorial will help you visualize key market structure components like Value Areas, Points of Control (POC), and TPO (Time Price Opportunity) Counts.

Here’s what you’ll learn:

  • How to retrieve minute-level market data using the TraderMade API
  • How to calculate Market Profile elements such as Value Areas and POC
  • How to display your analysis interactively using Streamlit and Plotly

What You’ll Need Before You Start

To follow along, you should have a basic understanding of Python, some familiarity with Market Profile concepts, and access to a TraderMade Forex API key.

Let’s Get Started

We’ve broken down this tutorial into clear, manageable steps to make it easy to follow. First, we’ll begin by setting up your environment.

Step 1: Setting Up Your Environment

Make sure the following tools and libraries are installed:

pip install streamlit plotly pandas numpy python-dateutil tradermade 
Enter fullscreen mode Exit fullscreen mode

Getting Started

Begin by importing the necessary libraries and setting up the API connection.

import streamlit as st import plotly.graph_objects as go import pandas as pd import numpy as np import datetime from dateutil.relativedelta import relativedelta import streamlit.components.v1 as components import tradermade as tm tm.set_rest_api_key('Your_API_Key') # Replace with your TraderMade API Key 
Enter fullscreen mode Exit fullscreen mode

Retrieve Minute-Level Data Using the TraderMade API

In this step, we'll create a function to fetch the market data required for building our Market Profile.

def get_data_raw(currency_pair, start_time, end_time, interval='minute'): try: # Fetch data using TraderMade SDK data = tm.timeseries( currency=currency_pair, start=start_time, end=end_time, interval=interval, period=30, fields=["open", "high", "low", "close"] ) # print(data) # Print the response to see its structure # print("API Response:", data) # Convert the response directly into a DataFrame df = pd.DataFrame(data) df['volume'] = df['close'] # Convert the 'date' column to datetime if it exists df = df.replace(0, np.nan) df = df.dropna() df.set_index("date", inplace=True) df = np.round(df, 4) if 'date' in df.columns: df['date'] = pd.to_datetime(df['date']) df.set_index('date', inplace=True) # Sort the DataFrame by index df.sort_index(inplace=True) return df except KeyError as e: print(f"KeyError: {e}") return pd.DataFrame() # Return an empty DataFrame in case of an error except Exception as e: print(f"An error occurred: {e}") return pd.DataFrame() # Return an empty DataFrame in case of a general error def get_data_MP(g, p, f, F, cur, st, ed): df = get_data_raw(cur, st, ed) return df 
Enter fullscreen mode Exit fullscreen mode

Create Helper Functions

We’ll start by defining a function that identifies the most recent working day—this helps us skip non-trading days like weekends.

def last_work_date(date, format): if datetime.datetime.strptime(date, "%Y-%m-%d").weekday() == 6: return (datetime.datetime.strptime(date, "%Y-%m-%d")-relativedelta(days=2)).strftime(format) elif datetime.datetime.strptime(date, "%Y-%m-%d").weekday() == 0: return (datetime.datetime.strptime(date, "%Y-%m-%d")-relativedelta(days=3)).strftime(format) else: return (datetime.datetime.strptime(date, "%Y-%m-%d")-relativedelta(days=1)).strftime(format) 
Enter fullscreen mode Exit fullscreen mode

Next, we'll define two additional functions to set the desired level of detail for our TPO and Market Profile views.

def get_rd(currency): od_list = ["UKOIL", "OIL", "XAGUSD","EVAUST","LUNUST",'LTCUST','XMRUST'] od2_list = ["JPY"] od3_list = ["AAPL", "AMZN", "NFLX", "TSLA", "GOOGL", "BABA", "TWTR", "BAC", "BIDU", 'XAUUSD','SOLUST','BNBUST','BCHUST','DSHUST','EGLUST'] od4_list = ["UK100", "SPX500","FRA40", "GER30","JPN225","NAS100","USA30", "HKG33", "AUS200","BTC","BTCUST",'ETHUSD',"ETHUST"] cfd_list = ["EURNOK", "EURSEK", "USDSEK","USDNOK","NEOUST","ETCUST","DOTUST", "UNIUST"] if currency in od_list or currency[3:6] in od2_list: ad = .01 rd = 2 elif currency in od3_list: ad = 0.1 rd = 1 elif currency in od4_list: ad = 2 rd = 0 elif currency in cfd_list: ad = .001 rd = 3 else: ad = 0.0001 rd = 4 return rd, ad def get_ad(ad, max, min): if (max-min) > ad*2000: return ad*50 if (max-min) > ad*1000: return ad*20 if (max-min) > ad*300: return ad*5 elif (max-min) > ad*100: return ad*2 else: return ad 
Enter fullscreen mode Exit fullscreen mode

Compute TPO and Value Area

The first function, midmax_idx, helps identify the index of the highest value in an array that’s closest to its midpoint. The second function, calculate_value_area, builds a value area around the Point of Control (POC) within the market profile (mp).

It does this by accumulating TPO counts from price levels near the POC until the sum reaches a specified target volume (target_vol). The function then returns the price range—from min_idx to max_idx—that defines the value area. Note that this refers to TPO volume, which differs from actual trading volume.

def midmax_idx(array): if len(array) == 0: return None # Find candidate maxima # maxima_idxs = np.argwhere(array.to_numpy() == np.amax(array.to_numpy()))[:, 0] maxima_idxs = np.argwhere(array == np.amax(array))[:,0] if len(maxima_idxs) == 1: return maxima_idxs[0] elif len(maxima_idxs) <= 1: return None # Find the distances from the midpoint to find # the maxima with the least distance midpoint = len(array) / 2 v_norm = np.vectorize(np.linalg.norm) maximum_idx = np.argmin(v_norm(maxima_idxs - midpoint)) return maxima_idxs[maximum_idx] def calculate_value_area(poc_volume, target_vol, poc_idx, mp): min_idx = poc_idx max_idx = poc_idx while poc_volume < target_vol: last_min = min_idx last_max = max_idx next_min_idx = np.clip(min_idx - 1, 0, len(mp) - 1) next_max_idx = np.clip(max_idx + 1, 0, len(mp) - 1) low_volume = mp.iloc[next_min_idx].vol if next_min_idx != last_min else None high_volume = mp.iloc[next_max_idx].vol if next_max_idx != last_max else None if not high_volume or (low_volume and low_volume > high_volume): poc_volume += low_volume min_idx = next_min_idx elif not low_volume or (high_volume and low_volume <= high_volume): poc_volume += high_volume max_idx = next_max_idx else: break return mp.iloc[min_idx].value, mp.iloc[max_idx].value mp_dict = {"0000":"A","0030":"B","0100":"C","0130":"D","0200":"E","0230":"F","0300":"G","0330":"H","0400":"I","0430":"J","0500":"K","0530":"L","0600":"M","0630":"N","0700":"O","0730":"P","0800":"Q","0830":"R","0900":"S","0930":"T","1000":"U","1030":"V","1100":"W","1130":"X","1200":"a","1230":"b","1300":"c","1330":"d","1400":"e","1430":"f","1500":"g","1530":"h","1600":"i","1630":"j","1700":"k","1730":"l","1800":"m","1830":"n","1900":"o","1930":"p","2000":"q","2030":"r","2100":"s","2130":"t","2200":"u","2230":"v","2300":"w","2330":"x",} 
Enter fullscreen mode Exit fullscreen mode

Bringing It All Together

To wrap things up, we’ll use the following function to generate the complete Market Profile.

def cal_mar_pro(currency, study, freq, period, mode, fp, mp_st, mp_ed, date): st = datetime.datetime.strptime(mp_st, "%Y-%m-%d-%H:%M") print(currency, mp_st, mp_ed) # try: rf = get_data_MP("M", str(freq), "%Y-%m-%d-%H:%M" , "%Y-%m-%d-%H:%M",currency, mp_st, mp_ed) hf = rf.copy() last_price = rf.iloc[-1]["close"] rf = rf[["high","low"]] rf.index = pd.to_datetime(rf.index) #rf = rf[:96] rd, ad = get_rd(currency) max = round(rf.high.max(), rd) min = round(rf.low.min(), rd) ad = get_ad(ad, max, min) try: x_data = np.round(np.arange(min, max, ad).tolist(), rd) except: print("MP loop failed", currency) # x_data = x_data[::-1] y_data= [] z_data = [] y_data1= [] z_data1 = [] tocount1 = 0 for item in x_data: alpha = "" alpha1 = "" alpha2 = "" for i in range(len(rf)): if rf.index[i] < date: if round(rf.iloc[i]["high"], rd) >= item >= round(rf.iloc[i]["low"], rd): alpha += mp_dict[rf.index[i].strftime("%H%M")] elif rf.index[i] >= date: if round(rf.iloc[i]["high"], rd) >= item >= round(rf.iloc[i]["low"], rd): alpha1 += mp_dict[rf.index[i].strftime("%H%M")] tocount1 += len(alpha1) y_data.append(len(alpha)) y_data1.append(len(alpha1)) z_data.append(alpha) z_data1.append(alpha1) # y_data = y_data[::-1] # y_data1 = y_data1[::-1] mp = pd.DataFrame([x_data, y_data1]).T mp = mp[::-1] mp = mp.rename(columns={0:"value",1:"vol"}) #poc_idx = midmax_idx(mp.vol) poc_idx = midmax_idx(mp.vol.values) poc_vol = mp.iloc[poc_idx].vol poc_price = mp.iloc[poc_idx].value target_vol = 0.7*tocount1 value_high,value_low = calculate_value_area(poc_vol, target_vol, poc_idx, mp) print("Value area",tocount1, 0.7*tocount1) return x_data, y_data,y_data1, z_data, z_data1, value_high, value_low, poc_price, last_price, hf 
Enter fullscreen mode Exit fullscreen mode

Now that we’ve set up the Market Profile data, it’s time to build the dashboard.

Streamlit Dashboard

We’ll structure the Streamlit app into two main sections: the sidebar and the main display area. The sidebar will handle user inputs and configuration options to make the dashboard interactive, while the main area will present the Market Profile charts and tables.

Sidebar

Let’s start by setting up the sidebar—it’s straightforward and intuitive.

# Streamlit code to take input st.set_page_config(layout="wide") st.title("Market Profile Dashboard") # currency = st.sidebar.text_input("Enter Currency Pair (e.g., EURUSD):", "EURUSD") category = st.sidebar.selectbox("Select Category", ["CFD", "Forex"], index=["CFD", "Forex"].index(st.session_state.category)) item_list = tm.cfd_list() if category == "CFD" else ["EURUSD", "GBPUSD", "AUDUSD", "USDCAD", "EURNOK", "USDJPY", "EURGBP", "BTCUSD", "USDCHF", "NZDUSD", "USDINR", "USDZAR", "ETHUSD", "EURSEK"] # Ensure item_list is a list and contains the default currency if isinstance(item_list, list) and st.session_state.currency in item_list: index = item_list.index(st.session_state.currency) else: index = 0 # Default to the first item if the currency is not in the list currency = st.sidebar.selectbox("Select an Item", item_list, index=index) study = st.sidebar.selectbox("Select Study Type:", ["MP"]) date = st.sidebar.date_input("Select Date") # Subtract one day using relativedelta to get the previous day at 00:00 hours mp_st = last_work_date(date.strftime('%Y-%m-%d'), '%Y-%m-%d-00:00') if date != datetime.datetime.now().date(): mp_ed = date.strftime('%Y-%m-%d-23:59') else: mp_ed = datetime.datetime.now().strftime('%Y-%m-%d-%H:%M') # Convert to string format # mp_st = previous_day.strftime("%Y-%m-%d-%H:%M") freq = st.sidebar.selectbox("Select Minutes per period:", [30]) period = st.sidebar.selectbox("Select periods:", [24]) mode = st.sidebar.selectbox("Select Mode:", ["tpo"]) fp = int((60/int(freq))*period) if date.weekday() in [5, 6]: # 5 = Saturday, 6 = Sunday st.warning("Please select a weekday (Monday to Friday). Weekends are not allowed.") st.stop() # Stop the app execution if the date is a weekend # Check if the button has been clicked using session state if "button_clicked" not in st.session_state: st.session_state.button_clicked = False # Create the Market Profile chart only when the button is clicked if st.sidebar.button("Get Market Profile"): st.session_state.button_clicked = True 
Enter fullscreen mode Exit fullscreen mode

Visualize with Streamlit & Plotly

Before we render the charts and TPO Profile, we’ll check if the user has clicked the button. Once confirmed, we’ll go ahead and display the visual output.

if st.session_state.button_clicked: #Get Market profile x_data, y_data,y_data1, z_data, z_data1, value_high, value_low, poc_price, last_price, hf = cal_mar_pro(currency, study, freq, period, mode, fp, mp_st,mp_ed, date) # Separate data for the previous day and current day previous_day_prices = x_data current_day_prices = x_data previous_day_counts = y_data current_day_counts = y_data1 # Create the Market Profile chart for previous day candlestick = go.Candlestick( x=hf.index, open=hf['open'], high=hf['high'], low=hf['low'], close=hf['close'], name=f'{currency} Candlestick Chart', ) tpo_dict = {} # Map index to date, using '00:00' hours formatting if necessary for i in range(len(hf.index)): tpo_dict[i] = hf.index[i] previous_day_dates = [] current_day_dates = [] for i in previous_day_counts: previous_day_dates.append(tpo_dict[i]) for i in current_day_counts: current_day_dates.append(tpo_dict[i]) # Create a vertical bar chart using the same x-axis (dates) and y-axis (volume) bar_chart = go.Bar( x=previous_day_dates, # Use the same dates for x-axis y=previous_day_prices, # Volume data for y-axis orientation='h', name="TPO Count previous day", marker=dict( color='rgba(50, 171, 96, 0.6)', line=dict(color='rgba(50, 171, 96, 1.0)', width=1) ), opacity=0.5, # Adjust opacity to make both charts visible hoverinfo='skip' ) bar_chart2 = go.Bar( x=current_day_dates, # Use the same dates for x-axis y=current_day_prices, # Volume data for y-axis orientation='h', name=f"TPO Count {date}", marker=dict( color='rgba(100, 149, 237, 0.6)', line=dict(color='rgba(100, 149, 237, 1.0)', width=1), ), opacity=0.5, # Adjust opacity to make both charts visible hoverinfo='skip' ) # Initialize the figure and add both traces fig1 = go.Figure() fig1.add_trace(candlestick) fig1.add_trace(bar_chart) fig1.add_trace(bar_chart2) print(y_data[0],y_data[-1:], poc_price) num_ticks = 6 # Number of ticks to display tick_indices = np.linspace(0, len(hf.index) - 1, num_ticks, dtype=int) tickvals = [hf.index[i] for i in tick_indices] ticktext = [hf.index[i] for i in tick_indices] # Update x-axis to show fewer date labels fig1.update_xaxes( tickvals=tickvals, # Specify which dates to show ticktext=ticktext, # Specify the labels for those dates tickangle=0, # Keep the tick labels horizontal title_text='Date', showgrid=True, ) fig1.update_yaxes( showgrid=True, ) # Update layout for the combined chart fig1.update_layout( title=f'{currency} Candlestick Chart with TPO Bars', xaxis_title='Date/TPO Count', yaxis_title='Price', yaxis=dict(range=[x_data[0], x_data[-1]]), # Reverse the y-axis height=600, xaxis=dict( type='category', # Use 'category' type to skip missing dates categoryorder='category ascending', # Order categories in ascending order showgrid=True, ), xaxis_rangeslider_visible=False # Hide the range slider ) # Create the Market Profile chart for current day fig2 = go.Figure() fig2.add_trace(go.Bar( x=previous_day_counts, # Count on the x-axis y=previous_day_prices, # Price levels on the y-axis orientation='h', name=f"TPO Count Previous day", marker=dict( color='rgba(50, 171, 96, 0.6)', line=dict(color='rgba(50, 171, 96, 1.0)', width=1) ), opacity=0.5 )) fig2.add_trace(go.Bar( x=current_day_counts, # Count on the x-axis y=current_day_prices, # Price levels on the y-axis orientation='h', name=f"TPO Count {date}", marker=dict( color='rgba(100, 149, 237, 0.6)', line=dict(color='rgba(100, 149, 237, 1.0)', width=1), ), )) fig2.update_xaxes( showgrid=True, ) fig2.update_yaxes( showgrid=True, ) fig2.update_layout( title=f"{currency} Two-day Market Profile", xaxis_title="TPO Count", yaxis_title="Price Levels", template="plotly_white", height=600, width=600 ) # Create two columns to display charts side by side col1, col2 = st.columns([1, 1]) with col1: st.plotly_chart(fig1, use_container_width=True) # Adjusts the chart to fit the column width with col2: st.plotly_chart(fig2, use_container_width=True) style = """ <style> table { width: 100%; border-collapse: collapse; font-family: 'Courier New', Courier, monospace; /* Monospaced font */ } th, td { border: 1px solid #ddd; padding: 8px; text-align: left; white-space: nowrap; /* Ensure no text wrapping */ font-weight: bold; } th { background-color: #f2f2f2; font-weight: bold; } tr:nth-child(even) { background-color: #f9f9f9; /* Light gray background for even rows */ } tr:hover { background-color: #f1f1f1; /* Highlight row on hover */ } </style>""" html_table = style html_table += "<table style='width:100%; border: 1px solid black; font-size: 12px; border-collapse: collapse;font-family: 'Courier New', Courier, monospace;'>" html_table += "<tr><th style='border: 1px solid black; padding: 2px;'>Prices</th>" html_table += "<th style='border: 1px solid black;padding: 2px;'>Previous Day</th>" html_table += f"<th style='border: 1px solid black;padding: 2px;'>{date}</th></tr>" # Populate the table rows for x, z, z1 in zip(reversed(x_data), reversed(z_data), reversed(z_data1)): if x in [value_low, poc_price, value_high]: html_table += f"<tr><td style='color:#FF6961; border: 1px solid black; padding: 0px;'>{x}</td>" html_table += f"<td style='color:#FF6961; border: 1px solid black; padding: 0px;'>{z}</td>" html_table += f"<td style='color:#FF6961; border: 1px solid black; padding: 0px;'>{z1}</td></tr>" else: html_table += f"<tr><td style='border: 1px solid black; padding: 0px;'>{x}</td>" html_table += f"<td style='border: 1px solid black; padding: 0px;'>{z}</td>" html_table += f"<td style='border: 1px solid black; padding: 0px;'>{z1}</td></tr>" html_table += "</table>" html_table2 = style html_table2 += f""" <table style='width:100%; border: 1px solid black; border-collapse: collapse; font-size: 10px;'> <tr> <th style='border: 1px solid black; padding: 2px;'>Letter</th> <th style='border: 1px solid black; padding: 2px;'>Period</th> </tr> """ # Populate the second table rows in reverse order or different formatting for letter, period in mp_dict.items(): html_table2 += f""" <tr> <td style='border: 1px solid black; padding: 2px;'>{letter}</td> <td style='border: 1px solid black; padding: 2px;'>{period}</td> </tr> """ html_table2 += f"</table>" html_table3 = style html_table3 += f""" <table style='width:100%; border: 1px solid black; border-collapse: collapse; font-size: 10px;'> <tr> <th style='border: 1px solid black; padding: 2px;'>Key Prices</th> <th style='border: 1px solid black; padding: 2px;'>Values</th> </tr> """ html_table3 += f""" <tr> <td style='border: 1px solid black; padding: 2px;'>Value Area High</td> <td style='border: 1px solid black; padding: 2px;'>{value_high}</td> </tr> <tr> <td style='border: 1px solid black; padding: 2px;'>Point of Control (POC)</td> <td style='border: 1px solid black; padding: 2px;'>{poc_price}</td> </tr> <tr> <td style='border: 1px solid black; padding: 2px;'>Value Area Low</td> <td style='border: 1px solid black; padding: 2px;'>{value_low}</td> </tr> </table> """ col3, col4 = st.columns([2, 2]) with col1: components.html(html_table, height=2000, scrolling=True) with col2: components.html(html_table3,height=90, scrolling=True) components.html(html_table2, height=1200, scrolling=True) 
Enter fullscreen mode Exit fullscreen mode

The final section of the code is somewhat lengthy, but it primarily focuses on displaying the data using HTML tables and Plotly charts. If you're interested in customizing or learning more, you can explore Plotly and HTML documentation. For now, feel free to copy and paste this part as is.

Wrapping Up

You now have a fully functional and interactive Market Profile dashboard that works with Forex, CFD, and Crypto data. In just a few minutes, you can analyze different currency pairs, timeframes, and market structures.

To view the complete code, head over to our GitHub Market Profile examples page.

You can also enhance your dashboard by:

  • Experimenting with different time intervals, such as hourly or daily
  • Incorporating Volume Profile analysis
  • Adding alerts or triggers based on your trading strategies

Please go through the original tutorial on our website:
Build a Market Profile Dashboard with Python

Top comments (0)