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:
- 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.
- 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