Skip to content

Commit

Permalink
Update insights page on app to enhance portfolio metrics. Fix app bugs.
Browse files Browse the repository at this point in the history
  • Loading branch information
Francisco Silva committed Feb 20, 2025
1 parent 6c38c8b commit 574fe4b
Show file tree
Hide file tree
Showing 6 changed files with 186 additions and 71 deletions.
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ dependencies = [
"shap",
"pre-commit",
"pydantic",
"openpyxl"
"openpyxl",
"optuna"
]

[project.optional-dependencies]
Expand Down
54 changes: 46 additions & 8 deletions stocksense/app/home.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,60 @@ def run():

st.set_page_config(
layout="wide",
page_title="Stocksense Home",
page_icon="🏠",
page_title="Stocksense",
page_icon="📈",
initial_sidebar_state="expanded",
)
st.sidebar.title("Stocksense App")
st.sidebar.success("Select page")

st.sidebar.page_link("home.py", label="Home", icon="🏠")
st.sidebar.page_link("pages/overview.py", label="Market Overview", icon="🌎")
st.sidebar.page_link("pages/analytics.py", label="Stock Analytics", icon="📈")
st.sidebar.page_link("pages/insights.py", label="Stock Picks", icon="🔮")
st.sidebar.page_link("pages/insights.py", label="Stock Picks", icon="💼")
st.sidebar.divider()
st.header(
"""
Welcome to Stocksense Analytics App!
"""
)

st.header("Welcome to Stocksense Analytics App!")
st.divider()

# Overview section
st.subheader("📊 App Features")

col1, col2 = st.columns(2)

with col1:
st.markdown("### 🌎 Market Overview")
st.markdown("""
- Real-time S&P 500 market summary
- Sector distribution analysis
- Latest earnings reports tracking
- Key market metrics and statistics
""")

st.markdown("### 📈 Stock Analytics")
st.markdown("""
- Detailed individual stock analysis
- Technical and fundamental indicators
- Financial statements analysis
- Insider trading tracking
""")

with col2:
st.markdown("### 💼 Portfolio Insights")
st.markdown("""
- Model-driven stock selection
- Portfolio composition analysis
- Sector allocation visualization
- Performance metrics tracking
""")

st.markdown("### 🔍 Key Features")
st.markdown("""
- Interactive data visualization
- Accurate market data
- Curated financial metrics
- User-friendly interface
""")


def main():
Expand Down
2 changes: 1 addition & 1 deletion stocksense/app/pages/analytics.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ def main():
st.sidebar.page_link("home.py", label="Home", icon="🏠")
st.sidebar.page_link("pages/overview.py", label="Market Overview", icon="🌎")
st.sidebar.page_link("pages/analytics.py", label="Stock Analytics", icon="📈")
st.sidebar.page_link("pages/insights.py", label="Stock Picks", icon="🔮")
st.sidebar.page_link("pages/insights.py", label="Stock Picks", icon="💼")
st.sidebar.divider()
st.sidebar.header("Options: ")

Expand Down
143 changes: 99 additions & 44 deletions stocksense/app/pages/insights.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,94 +18,149 @@ def load_stock_data():
return db.fetch_stock().to_pandas()


def get_available_dates():
def get_available_portfolios():
"""
Get all available trade dates from score files.
Get all available portfolio files.
"""
score_files = list(SCORES_DIR.glob("scores_*.csv"))
dates = [dt.datetime.strptime(f.stem.split("_")[1], "%Y-%m-%d").date() for f in score_files]
portfolio_files = list(PORTFOLIOS_DIR.glob("portfolio_*.xlsx"))
dates = [dt.datetime.strptime(f.stem.split("_")[1], "%Y-%m-%d").date() for f in portfolio_files]
return sorted(dates, reverse=True)


def load_scores(trade_date):
def load_portfolio(trade_date):
"""
Load scores for a specific trade date.
Load portfolio for a specific trade date.
"""
score_file = SCORES_DIR / f"scores_{trade_date}.csv"
if not score_file.exists():
st.error(f"No scores found for trade date {trade_date}")
portfolio_file = PORTFOLIOS_DIR / f"portfolio_{trade_date}.xlsx"
if not portfolio_file.exists():
st.error(f"No portfolio found for trade date {trade_date}")
return None
return pd.read_csv(score_file)
return pd.read_excel(portfolio_file)


def plot_sector_distribution(portfolio_data):
"""
Plot sector distribution of selected stocks.
"""
sector_dist = portfolio_data.groupby("sector")["weight"].sum().reset_index()
sector_dist = portfolio_data.groupby("Sector")["Weight"].sum().reset_index()
fig = px.pie(
sector_dist,
values="weight",
names="sector",
values="Weight",
names="Sector",
title="Sector Distribution",
template="plotly_dark",
)
fig.update_traces(textposition="inside", textinfo="percent+label")
st.plotly_chart(fig, use_container_width=True)
return fig


def plot_weight_distribution(portfolio_data):
"""
Plot weight distribution of top 10 stocks.
"""
top_10 = portfolio_data.nlargest(10, "Weight")
fig = px.pie(
top_10,
values="Weight",
names="Ticker",
title="Top 10 Holdings by Weight",
template="plotly_dark",
)
fig.update_traces(textposition="inside", textinfo="percent+label")
return fig


def display_portfolio_metrics(portfolio_data):
"""
Display key portfolio metrics.
"""
total_stocks = len(portfolio_data)
avg_score = portfolio_data["pred"].mean()
avg_price = portfolio_data["adj_close"].mean()
# Convert percentage strings to floats for calculations
portfolio_data['Weight'] = portfolio_data['Weight'].str.rstrip('%').astype(float) / 100

avg_model_score = portfolio_data['Model Score'].mean()
num_sectors = portfolio_data['Sector'].nunique()

col1, col2, col3 = st.columns(3)
col1, col2 = st.columns(2)
with col1:
st.metric("Number of Stocks", total_stocks)
st.metric("Average Model Score", f"{avg_model_score:.1f}")
with col2:
st.metric("Average Model Score", f"{avg_score:.3f}")
with col3:
st.metric("Average Stock Price", f"${avg_price:.2f}")
st.metric("Sectors", num_sectors)


def main():
"""Insights main script."""
st.set_page_config(layout="wide", page_title="Portfolio Insights", page_icon="💼")

st.set_page_config(layout="wide", page_title="Stock Picks", page_icon="🔮")
# Sidebar navigation
st.sidebar.title("Stocksense App")
st.sidebar.success("Select page")

st.sidebar.page_link("home.py", label="Home", icon="🏠")
st.sidebar.page_link("pages/overview.py", label="Market Overview", icon="🌎")
st.sidebar.page_link("pages/analytics.py", label="Stock Analytics", icon="📈")
st.sidebar.page_link("pages/insights.py", label="Stock Picks", icon="🔮")
st.sidebar.page_link("pages/insights.py", label="Portfolio Insights", icon="💼")
st.sidebar.divider()

st.title("Stock Picks Insights")
# Main content
st.title("Portfolio Insights 💼")
st.divider()

stock_data = load_stock_data()
available_dates = get_available_dates()
trade_date = st.selectbox("Select Trade Date", available_dates)
scores = load_scores(trade_date)
scores = scores.join(stock_data, on="tic", rsuffix="_stock")
# Portfolio selection
available_dates = get_available_portfolios()
if not available_dates:
st.warning("No portfolio files found.")
return

if scores is not None:
display_portfolio_metrics(scores)
plot_sector_distribution(scores)

st.subheader("Top 30 Selected Stocks")
columns_to_display = ["symbol", "company_name", "sector", "pred", "adj_close", "weight"]
formatted_scores = scores[columns_to_display].head(30)

formatted_scores["pred"] = formatted_scores["pred"].round(3)
formatted_scores["adj_close"] = formatted_scores["adj_close"].round(2)
formatted_scores["weight"] = (formatted_scores["weight"] * 100).round(2).astype(str) + "%"
formatted_scores.columns = ["Symbol", "Company", "Sector", "Score", "Price ($)", "Weight"]
trade_date = st.selectbox(
"Select Portfolio Date",
available_dates,
format_func=lambda x: x.strftime("%B %d, %Y")
)

st.dataframe(formatted_scores, use_container_width=True)
portfolio = load_portfolio(trade_date)

if portfolio is not None:
st.divider()
display_portfolio_metrics(portfolio)
st.divider()

col1, col2 = st.columns(2)
with col1:
weight_fig = plot_weight_distribution(portfolio)
st.plotly_chart(weight_fig, use_container_width=True)

with col2:
sector_fig = plot_sector_distribution(portfolio)
st.plotly_chart(sector_fig, use_container_width=True)

st.subheader("Portfolio Composition")
# Style the dataframe
portfolio["Weight"] = portfolio["Weight"].astype(float) * 100
st.dataframe(
portfolio,
column_config={
"Ticker": st.column_config.TextColumn("Ticker", width="small"),
"Company": st.column_config.TextColumn("Company", width="medium"),
"Sector": st.column_config.TextColumn("Sector", width="medium"),
"Strike Price ($)": st.column_config.NumberColumn(
"Strike Price ($)",
format="$%.2f",
width="small"
),
"Weight": st.column_config.NumberColumn(
"Weight",
format="%.2f%%",
width="small",
help="Portfolio weight in percentage"
),
"Model Score": st.column_config.NumberColumn(
"Model Score",
format="%.1f",
width="small"
),
},
hide_index=True,
use_container_width=True,
)
else:
st.warning("No data available for the selected date.")

Expand Down
2 changes: 1 addition & 1 deletion stocksense/app/pages/overview.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ def main():
st.sidebar.page_link("home.py", label="Home", icon="🏠")
st.sidebar.page_link("pages/overview.py", label="Market Overview", icon="🌎")
st.sidebar.page_link("pages/analytics.py", label="Stock Analytics", icon="📈")
st.sidebar.page_link("pages/insights.py", label="Stock Picks", icon="🔮")
st.sidebar.page_link("pages/insights.py", label="Stock Picks", icon="💼")
st.sidebar.divider()

data = load_sp500_data()
Expand Down
53 changes: 37 additions & 16 deletions stocksense/model/portfolio.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,31 +194,52 @@ def _save_portfolio_excel(self, portfolio: pl.DataFrame, trade_date: dt.datetime
Trade date.
"""
excel_path = self.portfolios_dir / f"portfolio_{trade_date.date()}.xlsx"

# Convert to pandas once and rename columns
portfolio_pd = portfolio.rename({
"tic": "Ticker",
"name": "Company",
"sector": "Sector",
"adj_close": "Strike Price ($)",
"mkt_cap": "Market Cap ($M)",
"avg_score": "Model Score",
"weight": "Weight",
"max_return_4Q": "Max Return 1Y",
"fwd_return_4Q": "Forward Return 1Y"
}, strict=False).to_pandas()

# Format numeric columns
portfolio_pd["Weight"] = portfolio_pd["Weight"].map("{:.2%}".format)
portfolio_pd["Model Score"] = portfolio_pd["Model Score"].round(2)
portfolio_pd["Market Cap ($M)"] = portfolio_pd["Market Cap ($M)"].round(2)
portfolio_pd["Strike Price ($)"] = portfolio_pd["Strike Price ($)"].round(1)
if "Max Return 1Y" in portfolio_pd.columns:
portfolio_pd["Max Return 1Y"] = portfolio_pd["Max Return 1Y"].round(2)
portfolio_pd["Forward Return 1Y"] = portfolio_pd["Forward Return 1Y"].round(2)

with pd.ExcelWriter(excel_path, engine="openpyxl") as writer:
# Sheet 1: Full Portfolio
portfolio_df = portfolio.sort("weight", descending=True).to_pandas()
portfolio_df["weight"] = portfolio_df["weight"].map("{:.2%}".format)
portfolio_df["avg_score"] = portfolio_df["avg_score"].round(2)
portfolio_df["mkt_cap"] = portfolio_df["mkt_cap"].round(2)
portfolio_df["adj_close"] = portfolio_df["adj_close"].round(1)
if "max_return_4Q" in portfolio_df.columns:
portfolio_df["max_return_4Q"] = portfolio_df["max_return_4Q"].round(2)
portfolio_df["fwd_return_4Q"] = portfolio_df["fwd_return_4Q"].round(2)
portfolio_df.to_excel(writer, sheet_name="Full Portfolio", index=False)
portfolio_pd.sort_values("Weight", ascending=False).to_excel(
writer, sheet_name="Full Portfolio", index=False
)

# Sheet 2: Sector Allocations
# Create a copy of the dataframe before formatting weights for aggregation
portfolio_numeric = portfolio_pd.copy()
portfolio_numeric["Weight"] = portfolio_pd["Weight"].str.rstrip('%').astype(float) / 100

sector_alloc = (
portfolio.group_by("sector")
.agg(pl.col("weight").sum())
.sort("weight", descending=True)
.to_pandas()
portfolio_numeric.groupby("Sector")["Weight"]
.sum()
.reset_index()
.sort_values("Weight", ascending=False)
)
sector_alloc["weight"] = sector_alloc["weight"].map("{:.2%}".format)
sector_alloc["Weight"] = sector_alloc["Weight"].map("{:.2%}".format)
sector_alloc.to_excel(writer, sheet_name="Sector Allocations", index=False)

# Sheet 3: Top Holdings
top_positions = portfolio.sort("weight", descending=True).head(5).to_pandas()
top_positions["weight"] = top_positions["weight"].map("{:.2%}".format)
# Use original dataframe with formatted weights
top_positions = portfolio_pd.sort_values("Weight", ascending=False).head(5)
top_positions.to_excel(writer, sheet_name="Top Holdings", index=False)

logger.info(f"Portfolio saved to {excel_path}")

0 comments on commit 574fe4b

Please sign in to comment.