S&P 500 Treemap Code (annotated)

import yfinance as yf
import pandas as pd
from time import sleep
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.base import MIMEBase
from email import encoders
import os
import kaleido
import ssl
import certifi
import smtplib
import numpy as np
import plotly.express as px
import plotly.io as pio
from datetime import datetime

# region S&P 500 STOCK TICKERS
# Configure SSL to use certifi
ssl._create_default_https_context = ssl._create_unverified_context

# Set the SSL_CERT_FILE environment variable to the path of certifi's cacert.pem
os.environ['SSL_CERT_FILE'] = certifi.where()

# URL of the Wikipedia page containing the list of S&P 500 companies
url = 'https://en.wikipedia.org/wiki/List_of_S%26P_500_companies'

# Use pandas to read the HTML table from the Wikipedia page
tables = pd.read_html(url, header=0)

# Saves the first table
sp500_table = tables[0]

# Extract the 'Symbol' column which contains the tickers
tickers_list = sp500_table['Symbol'].tolist()

# Creates the string of tickers delimited by a space which is the format for yFinance
tickers_string = ' '.join(tickers_list)
# endregion

# region YFINANCE API, DICTIONARY CREATION, AND DF CREATION
tickers = yf.Tickers(tickers_string)

# sets up an empty dictionary that I'll add data to later
stock_data_dict = {'Name': [],
                   'Ticker': [],
                   'Sector': [],
                   'MarketCap': [],
                   'Open': [],
                   'Close': [],
                   'Change': []
                   }

# loops through list of stock tickers, grabs specific data, and appends that data to the dictionary defined above

x = 0  # X will be used to sleep function and avoid api limits
for stock in tickers_list:
    if x > 25:  # Every 25 loops, function sleeps for 5 seconds and X is reset to 0
        print('sleeping...')
        sleep(5)
        x = 0
    try:
        ticker = stock  # Saves the ticker
        company_info = tickers.tickers[stock].info  # Saves company info into dictionary
        stock_info = tickers.tickers[stock].history(period="5d")  # Saves stock info into dictionary
        name = company_info['shortName']  # Saves the stock info to variables
        sector = company_info['sector']
        market_cap = company_info['marketCap']
        previous_close_price = stock_info.Close.iloc[3]
        current_close_price = stock_info.Close.iloc[4]
        change = ((current_close_price - previous_close_price) / previous_close_price) * 100

        stock_data_dict['Name'].append(name)  # Appends the variables to their respective lists in dictionary
        stock_data_dict['Ticker'].append(ticker)
        stock_data_dict['Sector'].append(sector)
        stock_data_dict['MarketCap'].append(market_cap)
        stock_data_dict['Open'].append(previous_close_price)
        stock_data_dict['Close'].append(current_close_price)
        stock_data_dict['Change'].append(change)
        print(f'{ticker} | Prev Close {previous_close_price} | Current Close {current_close_price} | Return {change}')
        x += 1  # increments X
    except KeyError:  # handles the occasion where stock in Wiki list doesn't exist (ex: BRK.B)
        pass
    except IndexError:  # handles the occasion where stock in Wiki list doesn't exist (ex: BRK.B)
        pass

stock_df = pd.DataFrame.from_dict(stock_data_dict)  # Takes completed dictionary and saves it into a DF
stock_df = stock_df.drop_duplicates(subset=['Name'], keep='last')  # Drops any duplicated (off of company name)


stock_df.to_csv('stockdata_no_dups_removed.csv')
# endregion

# region CREATE AND FORMAT TREEMAP
# Define the colorscale for the treemap
colorscale = [
    [0, 'rgb(190, 68, 54)'],
    [0.5, 'rgb(228, 228, 228)'],
    [1, 'rgb(71, 113, 45)']
]


# Function to format MarketCap values
def format_marketcap(value):
    if value >= 1e12:
        return f'{value / 1e12:.2f}T'
    elif value >= 1e9:
        return f'{value / 1e9:.2f}B'
    else:
        return f'{value:.2f}'


# Apply the formatting function to the MarketCap and adds a new column names FormattedMarketCap
stock_df['FormattedMarketCap'] = stock_df['MarketCap'].apply(format_marketcap)

# Normalize the Change column to limit the color scale impact
stock_df['ChangeNormalized'] = np.clip(stock_df['Change'], -10, 10)


now = (datetime.now())
stock_df.to_csv(f'/Users/austinsteinmetz/PycharmProjects/Stock Treemap/Stock Data/stockdata {now}.csv')


# Create the treemap figure with custom data for hover info
fig = px.treemap(stock_df, path=[px.Constant("S&P 500"), 'Sector', 'Ticker'],  # Defines the hierarchy
                 values='MarketCap',  # determines the size of boxes
                 color='Change',  # determines the color coding
                 # Set the range for the color scale=colorscale,  # sets color coding to predefined colorscale
                 color_continuous_scale=colorscale,  # sets color coding to predefined colorscale
                 range_color=[-5, 5],
                 color_continuous_midpoint=0,  # sets color midpoint to 0%
                 hover_data=None,  # turns off default hover data, so I can set is custom later
                 custom_data=['Name', 'Change', 'FormattedMarketCap'])  # defines custom data used in the hover template

# Update the hover template to display 'Change' as a percentage and MarketCap in billions/trillions
fig.update_traces(hovertemplate='<b>%{customdata[0]}</b><br>Change: %{customdata[1]:.2f}%<br>Market Cap: %{'
                                'customdata[2]}')

# Update layout to adjust the margins
fig.update_layout(margin=dict(t=50, l=25, r=25, b=25))

# CREATE HTML AND SEND EMAIL
# Save the interactive HTML version

treemap_html = fig.to_html()  # creates an html object of the treemap
image_content = pio.to_image(fig, format='png', width=2400, height=1600, scale=2) # creates an image of the treemap


# Function that will be used to send the email
def send_email(sender_email, receiver_email, subject, body, smtp_server, smtp_port, login, app_password, html,
               image_content):
    # Create the email message
    msg = MIMEMultipart()
    msg['Subject'] = subject
    msg['From'] = sender_email
    msg['To'] = receiver_email
    msg.attach(MIMEText(body, 'plain'))
    part = MIMEBase("application", "octet-stream")
    part.set_payload(html.encode('utf-8'))
    encoders.encode_base64(part)
    part.add_header('Content-Disposition', 'attachment', filename='treemap_interactive.html')
    msg.attach(part)

    # Attach the image content as a file
    part = MIMEBase("application", "octet-stream")
    part.set_payload(image_content)
    encoders.encode_base64(part)
    part.add_header('Content-Disposition', 'attachment', filename='treemap.png')
    msg.attach(part)

    # Send the email via SMTP server
    with smtplib.SMTP_SSL(smtp_server, smtp_port) as server:
        server.login(login, app_password)
        server.send_message(msg)
    print("Email sent successfully.")


# Example usage with Gmail
send_email(
    sender_email="sender_emakil@gmail.com",
    receiver_email="receiver_email@gmail.com",
    subject="S&P 500 Treemap",
    body="S&P 500 Treemap",
    smtp_server="smtp.gmail.com",
    smtp_port=111,
    login="sender_emakil@gmail.com",
    app_password="PASSWORD",  # Use the app-specific password here
    html=treemap_html,
    image_content=image_content
)