Financial Market Dashboard Python Code


Performance Dashboard

The main objective of the Signal2Noise site is to provide a way for traders and investors to consume as much relevant information as possible in a snapshot.

The first thing you need is a table that produces returns over different time periods and presents them in a way that visually highlights a price move that is not a normal move. I use Z-Scores for colour coding, which I explain below.

Data Sourced & Presented

I use the symbol tickers from Yahoo Finance, a free resource, and the full name description. As you can see, I include the last traded price. I think it is always good to have a feel for the actual price of the symbol, but the most important information is the percentage return. I stay away from the point change, as it can be very deceptive. A 1,000-point move on the Dow sounds like a lot, which the financial media like to use, but it is only a 2% move at the current index price. 

There are two main problems when looking at % returns:

  1. The first issue is not knowing whether the move over a particular time period is a big or small move relative to its history. For example, the EURUSD moved 1% over the previous day. You think 1% is not a big deal, but in fact, a 1% move for EURUSD is a relatively big move.
  2. When you look at % returns in relation to other symbols, you cannot judge a 1% return as the same for all symbols. For example, a 1% move with EURUSD may be considered a big move, but a 1% move with Bitcoin is average.

To overcome these issues, we use the z-score signal to help us work out the signal 2 noise.  

Z-Score Signal

To overcome these issues, among a few others, we use the Z-Score to highlight whether there is a % return that is worth highlighting, what I call a signal.

The Z-Score formula is = (return – mean) / standard deviation 

The parameters I have used for the calculation of the table are as follows:

  • lookback period for the calculation of the mean and standard deviation is 5 years.
  • + 0.5 Z-Score is light green.
  • – 0.5 Z-Score is light coral. 
  • + 1 Z-Score is dark green
  • – 1 Z-Score is dark red.

The Code

In order to run the code you will need to have Python on your computer and the libraries I have included in the code block. They are all available via pip install.

I need to point out that when downloading certain data on exchanges in different time zones, Yahoo Finance has a built in function to download the data using UTC time according to the exchange location. 

I am running the data from Sydney, Australia, and it works fine as UTC doesn’t care. 

import yfinance as yf
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
import matplotlib.pyplot as plt
import matplotlib.ticker as mtick
from matplotlib.ticker import FuncFormatter
import seaborn as sns
import warnings

# Suppress runtime warnings
warnings.filterwarnings(“ignore”, category=RuntimeWarning)
warnings.filterwarnings(“ignore”, message=”The ‘unit’ keyword in TimedeltaIndex construction is deprecated”)

# Function to get the nearest date from a series of dates
def get_nearest_date(series, target):
    nearest = min(series, key=lambda x: abs(x – target))
    return nearest

# Define your watchlists
watchlists = {
    ‘main_watchlist’: [‘ES=F’, ‘^TNX’,’DX-Y.NYB’, ‘GC=F’, ‘CL=F’], # if you add ‘BTC-USD’ it will give not apply the colour shading correctly as BTC has a different timezone I cannot make it work when mixing with other signals.
    ‘main_stock_exchanges’: [‘^GSPC’,’^DJI’,’^IXIC’,’^VIX’, ‘^FTSE’, ‘^GDAXI’, ‘^N225’, ‘^HSI’, ‘000001.SS’, ‘^STI’, ‘^AORD’],
    ‘main_futures’ : [‘ES=F’,’ZN=F’, ‘GC=F’, ‘SI=F’,’PL=F’,’CL=F’, ‘HG=F’, ‘NG=F’, ‘ZC=F’, ‘KE=F’, ‘CC=F’, ‘KC=F’],
    ‘main_fx’ : [‘EURUSD=X’, ‘JPY=X’, ‘GBPUSD=X’, ‘AUDUSD=X’, ‘CNY=X’, ‘EURJPY=X’,’EURGBP=X’ ],
    ‘main_us_treasury -yield’ : [‘^IRX’, ‘^FVX’, ‘^TNX’, ‘^TYX’ ],
    ‘main_crypto’: [‘BTC-USD’, ‘ETH-USD’, ‘ADA-USD’, ‘SOL-USD’],
    ‘main_Mike_Stocks’: [‘AAPL’, ‘MSFT’, ‘GOOG’, ‘AMZN’, ‘TSLA’, ‘NVDA’,’META’],
  }

# Ticker to name mapping (extend this dictionary to include all your symbols)
descriptions = {
            ‘ES=F’  : ‘S&P 500 Future’,
            ‘^GSPC’  : ‘S&P 500 Index’,
            ‘^DJI’   : ‘Dow Jones Industrial Index’,
            ‘^IXIC’  : ‘Nasdaq 100 Index’,
            ‘^VIX’   : ‘VIX Index’,
            “TLT”    : ’20 Plus Year Treasury Bond ETF’,
            ‘ZN=F’   : ’10 Year T-Note (USA)’,
            ‘PL=F’   : ‘Platinum’,
            ‘BTC-USD’: ‘Bitcoin’,
            ‘ETH-USD’: ‘Ethereum’,
            ‘ADA-USD’: ‘Cardano’,
            ‘SOL-USD’: ‘Solana’,
            ‘DX-Y.NYB’: ‘US Dollar Index’,
            ‘GC=F’   : ‘Gold’,
            ‘CL=F’   : ‘Crude Oil’,
            ‘^FTSE’  : ‘FTSE 100 Index’,
            ‘^GDAXI’ : ‘DAX Index’,
            ‘^N225’  : ‘Nikkei 225 Index’,
            ‘^HSI’   : ‘Hang Seng Index’,
            ‘000001.SS’ : ‘Shanghai Composite Index’,
            ‘^STI’   : ‘Straits Times Index’,
            ‘^AORD’  : ‘Australian Stock Exchange’,
            ‘EURUSD=X’  : ‘EUR/USD’,
            ‘JPY=X’  : ‘JPY/USD’,
            ‘GBPUSD=X’  : ‘GBP/USD’,
            ‘AUDUSD=X’  : ‘AUD/USD’,
            ‘CNY=X’     : ‘CNY/USD’,
            ‘EURJPY=X’  : ‘EUR/JPY’,
            ‘EURGBP=X’  : ‘EUR/GBP’,
            ‘SI=F’     : ‘Silver’,
            ‘HG=F’    : ‘Copper’,
            ‘NG=F’    : ‘Natural Gas’,
            ‘ZC=F’    : ‘Corn’,
            ‘KE=F’    : ‘Wheat’,
            ‘CC=F’    : ‘Cocoa’,
            ‘KC=F’    : ‘Coffee’,
            ‘^IRX’    : ‘3 mnth Treasury yield’,
            ‘^FVX’    : ‘5 yr Treasury yield’,
            ‘^TNX’    : ’10 yr Treasury yield’,
            ‘^TYX’    : ’30 yr Treasury yield’,
            ‘BTC-USD’: ‘Bitcoin’,
            ‘ETH-USD’: ‘Ethereum’,
            ‘ADA-USD’: ‘Cardano’,
            ‘SOL-USD’: ‘Solana’,
            ‘AAPL’    : ‘Apple’,
            ‘MSFT’    : ‘Microsoft’,
            ‘GOOG’    : ‘Google’,
            ‘AMZN’    : ‘Amazon’,
            ‘TSLA’    : ‘Tesla’,
            ‘BABA’    : ‘Alibaba’,
            ‘NVDA’    : ‘NVIDIA’,
            ‘META’    : ‘Meta’,
}

# Download and process data for each watchlist
for watchlist_name, symbols in watchlists.items():
    print(f”Processing {watchlist_name}…”)

    # Get the current UTC datetime
    current_utc_datetime = datetime.utcnow()

    # Extract just the date part
    #end_date = current_utc_datetime.date()

    # Define the end date as today and start date as 20 years ago
    end_date = datetime.today()
    start_date = end_date – timedelta(days=252*20)  # Approximately 10 years of trading days

    # Download data
    data = yf.download(symbols, start=start_date, end=end_date)[‘Adj Close’]

    # Forward fill missing data points
    data.ffill(inplace=True)


    # Fill any remaining NaN values with 0
    data.fillna(0, inplace=True)

    # Compute returns
    returns = data.pct_change().dropna()

    # Compute different period returns
    returns_daily = data.pct_change().dropna()
    returns_weekly = data.resample(‘W’).ffill().pct_change().dropna()
    returns_monthly = data.resample(‘ME’).ffill().pct_change().dropna()
    returns_yearly = data.resample(‘YE’).ffill().pct_change().dropna()

    # Compute statistics
    stats_daily = {‘mean’: returns_daily.mean(), ‘std’: returns_daily.std()}
    stats_weekly = {‘mean’: returns_weekly.mean(), ‘std’: returns_weekly.std()}
    stats_monthly = {‘mean’: returns_monthly.mean(), ‘std’: returns_monthly.std()}
    stats_yearly = {‘mean’: returns_yearly.mean(), ‘std’: returns_yearly.std()}

    # Convert the index to datetime if it’s not already
    if not isinstance(data.index, pd.DatetimeIndex):
        data.index = pd.to_datetime(data.index)

    def get_last_business_day(target_date):
        “””
        Find the last business day for a given date. This function assumes that ‘data’ is a global DataFrame object with
        a DateTimeIndex.

        Parameters:
        target_date (datetime): The target date from which we want to find the last business day.

        Returns:
        datetime: The last business day.
        “””

        # Ensure the DataFrame index is a DateTimeIndex.
        if not isinstance(data.index, pd.DatetimeIndex):
            raise TypeError(“DataFrame index must be a DatetimeIndex”)

        # Create a date range ending with your target date, only including the business days in your data.
        # This line creates a DatetimeIndex of all business days in your dataset up to ‘target_date’.
        all_business_days = pd.date_range(start=data.index.min(), end=target_date, freq=’B’)

        # Find the intersection of this business days index with your data’s actual days to account for holidays/missing days.
        valid_business_days = data.index.intersection(all_business_days)

        if valid_business_days.empty:
            raise ValueError(“No business days found in the date range. Check your data’s date coverage.”)

        # The last valid business day is the maximum of this intersection.
        last_business_day = valid_business_days.max()

        return last_business_day

    # Calculate Date Offsets for Current Week, Month, and Year
    end_date = datetime.now()
    curr_week_start = get_nearest_date(data.index, end_date – timedelta(days=end_date.weekday() + 3))  # Previous Friday

    # Find the first day of the current month and subtract one day to get into the previous month
    first_day_current_month = end_date.replace(day=1)
    last_day_previous_month = first_day_current_month – timedelta(days=1)
    # Now find the last business day in the previous month
    curr_month_start = get_last_business_day(last_day_previous_month)

    # Find the first day of the current year and subtract one day to get into the previous year
    first_day_current_year = end_date.replace(month=1, day=1)
    last_day_previous_year = first_day_current_year – timedelta(days=1)
    # Now find the last business day in the previous year
    curr_year_start = get_last_business_day(last_day_previous_year)

    # Prepare data for final DataFrame (for each symbol in the current watchlist)
    df_data = []
    for symbol in symbols:
        price = f”{data[symbol].iloc[-1]:.2f}”  # Convert to string and keep 2 decimal places
        one_day_return = returns[symbol].iloc[-1]  # this is the current day’s return
        # Check if the return is zero and, if so, use the previous valid return.
        if one_day_return == 0:
            # Find the last non-zero return. This involves a bit of looping.
            # We start with the day before and go backwards until we find a non-zero return.
            idx = -2  # Start with the day before
            while True:
                one_day_return = returns[symbol].iloc[idx]
                if one_day_return != 0 or idx < -len(returns[symbol]):
                    break  # Stop if we find a non-zero return or reach the start of the series
                idx -= 1  # Move one day further back

        # Then convert the return to a percentage for display
        one_day_return_percentage = f”{one_day_return * 100:.2f}”  # same here
        week_return = f”{((data[symbol].iloc[-1] / data[symbol].loc[curr_week_start]) – 1) * 100:.2f}”  # and here
        month_return = f”{((data[symbol].iloc[-1] / data[symbol].loc[curr_month_start]) – 1) * 100:.2f}”  # and so on
        year_return = f”{((data[symbol].iloc[-1] / data[symbol].loc[curr_year_start]) – 1) * 100:.2f}”


        df_data.append({
            ‘Symbol’: symbol,
            ‘Description’: descriptions[symbol],
            ‘Price’: price,
            ‘1 Day %’: one_day_return_percentage,
            ‘Week %’: week_return,
            ‘Month %’: month_return,
            ‘Year %’: year_return,
        })
    # Create a DataFrame for the current watchlist
    df = pd.DataFrame(df_data)

    def color_map(idx, col_name):
        symbol = df.at[idx, ‘Symbol’]

        # Define a variable to capture the correct return, whether it’s the current day’s or a non-zero previous one.
        return_value = None

        # Determine the return value based on the column being processed.
        if col_name == ‘1 Day %’:
            return_value = returns_daily[symbol].iloc[-1]
            # If the return is zero, fetch the last non-zero return (similar to the earlier part of your code).
            if return_value == 0:
                idx = -2  # Start with the day before
                while True:
                    previous_return = returns_daily[symbol].iloc[idx]
                    if previous_return != 0 or idx < -len(returns_daily[symbol]):
                        return_value = previous_return  # Set the actual return to the found non-zero return.
                        break
                    idx -= 1  # Move one day further back

            # Now, calculate the Z-score using the actual return value.
            z_score = (return_value – stats_daily[‘mean’][symbol]) / stats_daily[‘std’][symbol]
        elif col_name == ‘Week %’:
            return_value = (data[symbol].iloc[-1] / data[symbol].loc[curr_week_start]) – 1
            z_score = (return_value – stats_weekly[‘mean’][symbol]) / stats_weekly[‘std’][symbol]
        elif col_name == ‘Month %’:
            return_value = (data[symbol].iloc[-1] / data[symbol].loc[curr_month_start]) – 1
            z_score = (return_value – stats_monthly[‘mean’][symbol]) / stats_monthly[‘std’][symbol]
        elif col_name == ‘Year %’:
            return_value = (data[symbol].iloc[-1] / data[symbol].loc[curr_year_start]) – 1
            z_score = (return_value – stats_yearly[‘mean’][symbol]) / stats_yearly[‘std’][symbol]
        else:
            return ‘white’  # If the column is not a return column.

        # Now we apply the logic for coloring based on the sign of the return value and its z-score.
        if return_value > 0:  # Positive return.
            if z_score > 1:
                return ‘darkgreen’
            elif z_score > 0.5:
                return ‘lightgreen’
            else:
                return ‘white’
        else:  # Negative return.
            if z_score < -1:
                return ‘darkred’
            elif z_score < -0.5:
                return ‘lightcoral’
            else:
                return ‘white’
        return determined_color  # This is whatever color your logic determines based on the Z-score.


    # Prepare colors DataFrame
    colors_df = pd.DataFrame(index=df.index, columns=df.columns)
    for idx, row in df.iterrows():
        for col in df.columns:
            colors_df.at[idx, col] = color_map(idx, col)

    # Define column widths and figure size
    col_widths = [0.05, 0.12, 0.05, 0.05, 0.05, 0.05, 0.05]  # Adjust as needed
    num_rows = len(symbols)
    row_height = 0.6  # Adjust as needed
    fig_height = num_rows * row_height

    # Create the figure and table
    fig, ax = plt.subplots(figsize=(14, fig_height))  # Adjust the overall size here
    ax.axis(‘off’)
    ax.set_title(f'{watchlist_name}’, fontsize=16, fontweight=’bold’)

    # Add last updated date and time
    last_updated = datetime.now().strftime(‘%Y-%m-%d %H:%M:%S’)
    ax.text(0, 1.02, f’Last Updated: {last_updated}’, transform=ax.transAxes, fontsize=9, va=’bottom’, ha=’left’)

    # Create the table
    tbl = ax.table(cellText=df.values, colLabels=df.columns, bbox=[0, 0, 1, 1],
                  cellColours=colors_df.values, colWidths=col_widths)
    tbl.auto_set_font_size(False)
    tbl.set_fontsize(13)  # Adjust the font size

    # Modify the cell height
    cellDict = tbl.get_celld()
    for i in range(len(df.columns)):
        cellDict[(0, i)].set_height(.012)  # Adjust the height
        for j in range(len(df.index)):
            cellDict[(j+1, i)].set_height(.01)  # Adjust the height

    # Make headers bold
    for (row, col), cell in tbl.get_celld().items():
        if row == 0:
            cell.set_text_props(fontweight=’bold’)

    # Formatting the end_date as a string in the format ‘YYYY-MM-DD’
    formatted_end_date = end_date.strftime(‘%Y-%m-%d’)

    # Save the figure to a file
    fig.tight_layout()
    plt.savefig(f’G:/My Drive/xxxx/{watchlist_name}.png’, bbox_inches=’tight’)
    # Include the formatted_end_date in the file name for Newsletter
    plt.savefig(f’G:/My Drive/xxxx/{watchlist_name}_{formatted_end_date}.png’, bbox_inches=’tight’)
    plt.show()

If you have any issues email me: michael@signal2noise.news