top of page

Building a Portfolio Optimization App with Streamlit and Python

  • Writer: Nikhil Adithyan
    Nikhil Adithyan
  • 6 days ago
  • 8 min read

A Step-by-Step Guide


ree

If you are building a fintech startup, the hardest part is rarely the idea. It is turning that idea into something real before time, money, or momentum runs out.


Most early-stage teams spend months setting up data pipelines, cleaning datasets, and wiring basic infrastructure. By the time a prototype is ready, the original problem you wanted to validate has already shifted. What you really need at this stage is not a perfect system, but a working one that proves your product is useful.


This article is written from that exact perspective. Instead of treating portfolio optimization as a purely academic exercise, I approach it as a product feature that could realistically sit inside a portfolio tracking or investment insights app. The goal is simple: show how a small team can build a usable portfolio optimizer and tracker without overengineering the stack.


Using Python, Streamlit, and market data from EODHD APIs, I walk through how to assemble a minimum viable portfolio tracking service. Not a demo that only works in theory, but a foundation you could actually extend into a real product.


Project Architecture

To create a responsive and interactive portfolio monitoring application, we will leverage Streamlit. Streamlit is a Python library designed for rapid web app development, making it ideal for proof-of-concept projects and early-stage prototypes. Rather than cramming all functionality into a single page, we will take advantage of Streamlit’s multipage feature to structure the app into clear, logical sections.


Here’s an example of how to organize a project:



Project/
  app.py
  pages/
    page_1.py   
    page_2.py 
    ...
  utils/
    metrics.py 
    plots.py

Fetching Data Using the EODHD API

The EODHD APIs offers a straightforward and efficient way to access a wide range of financial data, including historical prices, options data, news, and more. In this article, we will focus specifically on historical prices to calculate optimal portfolio allocations and news data for informed investment insights.


Fetching Historical Stock Prices


The EODHD Historical Prices API offers an easy and straightforward method to access historical stock prices. We will use the following Python functions:



API_KEY = "YOUR_EODHD_API_KEY"

from eodhd import APIClient
import pandas as pd 
import requests
from typing import List
import requests

def get_ticker_historical_prices(symbol: str):
    url = f'https://eodhd.com/api/eod/{symbol}?api_token={API_KEY}&fmt=json'
    data = requests.get(url).json()
    data = pd.DataFrame(data)
    data['ticker'] = symbol
    return data

def get_portfolio_historical_prices(symbols: List[str]):
    data = [get_ticker_historical_prices(symbol) for symbol in symbols]
    data = pd.concat(data)
    return data

Fetching Financial News & Events

The EODHD Calendar & News API offers a streamlined method for retrieving recent news events :



def get_ticker_news(ticker):
    url = f'https://eodhd.com/api/news?s={ticker}&offset=0&limit=100&api_token={API_KEY}&fmt=json'
    data = requests.get(url).json()
    data = pd.DataFrame(data)
    return data

News and prices fetching functions will be implemented in a data_fetching.py script inside the utils folder.


Using Streamlit

Streamlit provides comprehensive and clear documentation. Its syntax and philosophy (merged back and front end) allow for rapid implementation, making it ideal for building a first product version quickly, perfect for entrepreneurs or small startups. With one command in the terminal, you can launch your project in your local network and test it.



st.set_page_config(
    page_title="Welcome to Streamlit",
    page_icon="👋",
)

st.sidebar.success("Select a demo above.")

streamlit run app.py

ree

Building the App

When designing the app, it’s important to start with a high-level overview. How many pages do we need? What will be our main features? What is the data we need? How should the user interact with the product? The application will consist of three main pages: one for selecting tickers, one for computing optimal portfolio allocations, and one for monitoring news.


In this example, we aim to give users a way to determine optimal portfolio weights once they have decided which companies to invest in. Additionally, it provides the ability to track recent news for the stocks in their portfolio.


To achieve this, we will create three Python scripts: portfolio_definition.py for selecting tickers, portfolio_optimization.py for computing optimal portfolio weights, and headlines.py for monitoring news headlines. Additionally, we will include a utils folder to organize data fetching functions and other helper utilities.



Project/
  portfolio_definition.py
  pages/
    headlines.py   
    portfolio_optimization.py
  utils/
    data_fetching.py 
    data_cleaning.py

1. Portfolio Definition

To create a tab that allows the user to specify their tickers of interest, we can use the following implementation:



import streamlit as st 
import utils.data_fetching as data_fetching

st.title("Define Your Portfolio")

if "tickers" not in st.session_state:
    st.session_state.tickers = ["AAPL.US", "MSFT.US"]

new_ticker = st.text_input("Add new ticker:")

col1, col2 = st.columns([1, 3])

with col1:
    if st.button("Add"):
        if new_ticker and new_ticker.upper() not in st.session_state.tickers:
            st.session_state.tickers.append(new_ticker.upper())

with col2:
    for t in st.session_state.tickers:
        if st.button(f"Remove {t}", key=f"remove_{t}"):
            st.session_state.tickers.remove(t)
            st.experimental_rerun()

st.write("### Current Tickers")
st.write(st.session_state.tickers)

ree

2. Portfolio Optimization

To create a tab for optimizing a portfolio using the previously selected tickers, the following script is sufficient. It will be the main feature of the product.


Markovitz Parameters

We first define our page title and our sliders for parameter customization. We will allow the user to specify the training set period (for mean-variance optimization) and the test set, as well as the risk-free rate, which is required for optimization. For a more user-friendly interface, we can add captions under the parameters.



import pandas as pd
import numpy as np
import streamlit as st
from datetime import datetime
from utils.data_fetching import get_portfolio_historical_prices
from utils.data_cleaning import clean_dataset
import matplotlib.pyplot as plt
from scipy.optimize import minimize

st.title("Portfolio Optimisation")

# Load portfolio data
df = get_portfolio_historical_prices(st.session_state.tickers)
df = clean_dataset(df)

def sharpe_ratio(returns, risk_free_rate=0.02):
    mean_ret = returns.mean() * 252
    vol = returns.std() * np.sqrt(252)
    return (mean_ret - risk_free_rate) / vol

def portfolio_metrics(weights, mean_returns, cov_matrix, risk_free_rate):
    ret = np.dot(weights, mean_returns)
    vol = np.sqrt(weights.T @ cov_matrix @ weights)
    sharpe = (ret - risk_free_rate) / vol
    return np.array([ret, vol, sharpe])

def neg_sharpe(weights, mean_returns, cov_matrix, risk_free_rate):
    return -portfolio_metrics(weights, mean_returns, cov_matrix, risk_free_rate)[2]

# ----------------------------
# Sidebar / Main Layout Inputs
# ----------------------------
st.header('Markovitz Parameters')
col1, _, col2, _, col3, _ = st.columns([1, 0.1, 1, 0.1, 1, 0.1])

with col1:
    start_date = st.date_input("Start Date", datetime(2013, 1, 1))
    st.caption("Start date used to compute the efficient frontier")
with col2:
    end_date = st.date_input("End Date", datetime.today())
    st.caption("End date used to compute the efficient frontier")
with col3:
    r = st.number_input("Risk-free rate", value=0.02, step=0.001)

ree

Efficient Frontier

Then implement the efficient frontier, with mathematical formulas directly derived from Markowitz’s work.



# ----------------------------
# Filter Data by Date
# ----------------------------
mask = (df['date'] >= pd.to_datetime(start_date)) & (df['date'] <= pd.to_datetime(end_date))
mask_test = (df['date'] >= pd.to_datetime(end_date)) 
df_filtered = df[mask].copy()
df_test = df[mask_test].copy()

# ----------------------------
# Pivot Data: Date x Ticker
# ----------------------------
returns_df = df_filtered.pivot(index='date', columns='ticker', values='return')

# ----------------------------
# 1. Equal-Weight Portfolio
# ----------------------------
n_assets = len(returns_df.columns)
equal_weights = np.array([1/n_assets] * n_assets)

equal_portfolio_returns = returns_df.dot(equal_weights)
equal_cum_returns = (1 + equal_portfolio_returns).cumprod() - 1

# ----------------------------
# 2. Efficient Frontier / Optimal Portfolio (Max Sharpe)
# ----------------------------
mean_returns = returns_df.mean() * 252  # annualized
cov_matrix = returns_df.cov() * 252     # annualized

constraints = ({'type': 'eq', 'fun': lambda x: np.sum(x) - 1})
bounds = tuple((0, 1) for _ in range(n_assets))
result = minimize(neg_sharpe, equal_weights, args=(mean_returns, cov_matrix, r), 
                  method='SLSQP', bounds=bounds, constraints=constraints)

optimal_weights = result.x
optimal_portfolio_returns = returns_df.dot(optimal_weights)
optimal_cum_returns = (1 + optimal_portfolio_returns).cumprod() - 1

# ----------------------------
# Plotting
# ----------------------------

st.write("")
st.header("Optimal Portfolio Performance")

_, col1, _, col2, _ = st.columns([0.1, 1, 0.1, 1, 0.1])

with col1:
    fig, ax = plt.subplots(figsize=(10,6))
    equal_cum_returns.plot(ax=ax, label="Equal Weight Portfolio")
    optimal_cum_returns.plot(ax=ax, label="Efficient Portfolio (Max Sharpe)")
    ax.set_ylabel("Cumulative Returns")
    ax.set_xlabel("Date")
    ax.legend()
    ax.grid(True)
    st.subheader("Train Set")
    st.pyplot(fig)

    cumulative_return = optimal_cum_returns.iloc[-1]
    annual_sharpe = sharpe_ratio(optimal_portfolio_returns, r)

    cola, colb = st.columns([1, 1,])

    with cola:
        st.markdown(f"""
        <div style="text-align:center; font-size:14px; font-weight:bold;">
            Cumulative Return<br>
            <span style="font-size:18px; color:green;">{cumulative_return:.2%}</span>
        </div>
        """, unsafe_allow_html=True)

    with colb:
        st.markdown(f"""
        <div style="text-align:center; font-size:14px; font-weight:bold;">
            Sharpe Ratio<br>
            <span style="font-size:18px; color:blue;">{annual_sharpe:.2f}</span>
        </div>
        """, unsafe_allow_html=True)

with col2:
    if not df_test.empty:
        returns_test_df = df_test.pivot(index='date', columns='ticker', values='return')
        
        # Make sure test set has the same tickers
        returns_test_df = returns_test_df[returns_df.columns]  

        # Compute cumulative returns for optimal portfolio on test set
        optimal_test_returns = returns_test_df.dot(optimal_weights)
        optimal_test_cum_returns = (1 + optimal_test_returns).cumprod() - 1
        equal_weights_cum_returns = ((df_test.groupby('date')['return'].mean() + 1).cumprod() - 1)
        # Plot training vs test set performance
        st.subheader("Test Set")

        fig_test, ax_test = plt.subplots(figsize=(10,6))
        
        equal_weights_cum_returns.plot(ax=ax_test, label="Equal Weights Portfolio (Test Set)")
        optimal_test_cum_returns.plot(ax=ax_test, label="Optimal Portfolio (Test Set)")
        ax_test.set_ylabel("Cumulative Returns")
        ax_test.set_xlabel("Date")
        ax_test.legend()
        ax_test.grid(True)
        st.pyplot(fig_test)
    
        cumulative_return = optimal_test_cum_returns.iloc[-1]
        annual_sharpe = sharpe_ratio(optimal_test_returns, r)

        cola, colb = st.columns([1, 1,])

        with cola:
            st.markdown(f"""
            <div style="text-align:center; font-size:14px; font-weight:bold;">
                Cumulative Return<br>
                <span style="font-size:18px; color:green;">{cumulative_return:.2%}</span>
            </div>
            """, unsafe_allow_html=True)

        with colb:
            st.markdown(f"""
            <div style="text-align:center; font-size:14px; font-weight:bold;">
                Sharpe Ratio<br>
                <span style="font-size:18px; color:blue;">{annual_sharpe:.2f}</span>
            </div>
            """, unsafe_allow_html=True)
    else:
        st.info("No test set data available after the selected end date.")

optimal_weights_df = pd.DataFrame({
    "Ticker": returns_df.columns,
    "Weight": np.round(optimal_weights, 4)
})

overweight_threshold = 1 / len(optimal_weights) * 1.1  # 10% above equal weight
underweight_threshold = 1 / len(optimal_weights) * 0.9  # 10% below equal weight

def weight_status(w):
    if w > overweight_threshold:
        return "Overweight"
    elif w < underweight_threshold:
        return "Underweight"
    else:
        return "Neutral"

optimal_weights_df['Status'] = optimal_weights_df['Weight'].apply(weight_status)

def color_status(val):
    if val == "Overweight":
        color = 'green'
    elif val == "Underweight":
        color = 'red'
    else:
        color = 'gray'
    return f'color: {color}'

st.subheader("Optimal Portfolio Composition")
st.dataframe(
    optimal_weights_df.style.applymap(color_status, subset=['Status'])
)

Final Portfolio Optimizer Tab:


ree

We choose to display the train and test performance of the optimized portfolio and compare it to an equal-weight benchmark. Since the optimization objective is the portfolio Sharpe ratio, we report this metric to the user. Finally, to provide a valuable service, we will display the optimized portfolio weights.


3. Headlines

Finally, here is an example of a news reader for the tickers selected by the user.


Ticker selection

We first import our libraries and configure the titles and interactive section of Streamlit:



import pandas as pd
import numpy as np
import streamlit as st
from datetime import datetime
import json
from utils.data_fetching import get_ticker_news

st.set_page_config(page_title="📰 Recent Headlines", layout="wide")
st.title("📰 Recent Headlines")

# Ensure tickers exist in session_state
if "tickers" not in st.session_state or not st.session_state.tickers:
    st.warning("No tickers found in session state. Please load them first.")
else:
    selected_ticker = st.selectbox(
        "Select a ticker:",
        st.session_state.tickers,
        index=0  # default to first ticker
    )

    st.write(f"Selected ticker: **{selected_ticker}**")

    # Example: Fetch historical data
    with st.spinner(f"Fetching data for {selected_ticker}..."):
        df_news = get_ticker_news(selected_ticker)

ree

Newsreader

We then only need a small script to have a friendly newsreader:



columns = ["timestamp", "title", "content", "link", "tickers", "tags", "sentiment"]
df_news.columns = columns[:len(df_news.columns)]

# Convert timestamps safely
df_news["timestamp"] = pd.to_datetime(df_news["timestamp"], errors="coerce")

# Sort and keep most recent 10
df_news = df_news.sort_values("timestamp", ascending=False).head(10).reset_index(drop=True)

# ---- Styling helper ----
def sentiment_color(value):
    """Return background color for sentiment metrics."""
    if value < 0.05:
        return "#ffcccc"  # red
    elif value < 0.15:
        return "#fff2cc"  # yellow
    else:
        return "#d9ead3"  # green

# ---- Display news ----
for _, row in df_news.iterrows():
    title = str(row["title"]).strip()
    link = row["link"]
    ts = row["timestamp"]
    ts_str = ts.strftime("%Y-%m-%d %H:%M:%S") if pd.notnull(ts) else "Unknown date"
    tags = str(row.get("tags", "N/A"))
    content = str(row.get("content", ""))

    # Parse sentiment safely
    try:
        sent = json.loads(row["sentiment"])
        pos, neu, neg = sent.get("pos", 0), sent.get("neu", 0), sent.get("neg", 0)
    except Exception:
        pos, neu, neg = 0, 0, 0

    # ---- Card layout ----
    with st.container():
        st.markdown(
            f"""
            <div style="background-color:#f9f9f9; border-radius:12px; padding:20px; margin-bottom:20px;
                        box-shadow:0 0 6px rgba(0,0,0,0.1);">
                <h3 style="margin-bottom:6px;">
                    <a href="{link}" target="_blank" style="text-decoration:none; color:#1f77b4;">
                        {title}
                    </a>
                </h3>
                <p style="font-size:0.9em; color:#555;">
                    🕓 {ts_str}
                </p>
            </div>
            """,
            unsafe_allow_html=True
        )

        # Collapsible content
        with st.expander("Read more"):
            st.write(content)

        # Sentiment section
        col1, col2, col3 = st.columns(3)
        col1.markdown(
            f"<div style='background:{sentiment_color(pos)};padding:6px 10px;border-radius:8px;text-align:center;'>😊 Positive: {pos:.2f}</div>",
            unsafe_allow_html=True,
        )
        col2.markdown(
            f"<div style='background:{sentiment_color(neu)};padding:6px 10px;border-radius:8px;text-align:center;'>😐 Neutral: {neu:.2f}</div>",
            unsafe_allow_html=True,
        )
        col3.markdown(
            f"<div style='background:{sentiment_color(neg)};padding:6px 10px;border-radius:8px;text-align:center;'>☹️ Negative: {neg:.2f}</div>",
            unsafe_allow_html=True,
        )

        st.markdown("---")

ree

Here, we display the ten most recent news items for the stock selected by the user. Only their titles are shown initially, with an expandable sidebar allowing the user to view the full content. Since EODHD provides sentiment scores, we include these alongside each article.


Conclusion

This wasn’t written as a “learn Streamlit” tutorial. I’m treating it like a blueprint for a startup MVP.


If you’re planning to build a service around portfolio tracking or portfolio guidance, the fastest path is usually the simplest one. Get the core loop working first. Let users pick tickers. Pull clean price history. Compute allocations. Show performance. Add a news layer so the product feels alive and useful day to day.


That’s exactly what this app does. It gives you a working portfolio optimizer and tracker, with a clean multi-page structure that can be extended into a real product. EODHD APIs handles the part that usually slows teams down early on, which is reliable market data and news. That lets you spend your time on the product layer instead of stitching together messy sources.


If I were taking this forward as a startup, the next upgrades would be simple and product-driven. Add user accounts and saved portfolios. Add constraints like sector caps or max weight per stock. Add alerts for drawdowns, earnings, and major news tags. Then polish the UX and you already have something you can demo to users, or investors, without pretending it’s “just a prototype.”


Further Reading

  • Modern Portfolio Theory (MPT): To get a more in-depth understanding of MPT, consult Harry Markowitz’s work “Portfolio Selection”.

  • Streamlit Documentation: To learn how to build quickly interactive tools, explore the official Streamlit documentation.

  • EODHD Data Access: To find alternative data you can use in your product, consult the EODHD API.

Comments


Bring information-rich articles and research works straight to your inbox (it's not that hard). 

Thanks for subscribing!

© 2023 by InsightBig. Powered and secured by Wix

bottom of page