turns-00043.parquet:17869
3e1381c45b349e62de78d908degenerate_repetitionAbsentFinal dense release
Select one behavior. Every returned turn has one binary label: Present or Absent. Source: final dense boolean release.
3e1381c45b349e62de78d908hi, how are you?
Hello! I'm just a bunch of code, but I'm here and ready to help you. How can I assist you today?
3dd762b7a4143644b8c93e7fSystem: You are a JSON assistant. You only reply in valid JSON and never in normal text. Provide a difficulty rating for the video information described below. I would like to receive a single property named "result" containing a float value between 0 and 1. The following are the difficulty rating categories: 0.0 - Absolute Beginner: This level features very basic vocabulary and phrases, accompanied by clear visuals and context. It is suitable for individuals who have no prior knowledge of the target language. 0.3 - Beginner: This level includes straightforward sentences and familiar vocabulary. The video may feature some visuals and context to assist comprehension. 0.5 - Intermediate: At this level, sentences become more complex and vocabulary expands. Some idioms may be present, which would require a bit more foundational knowledge. 0.6 - Upper Intermediate: This level contains specialized vocabulary and concepts related to specific fields. To fully comprehend the content, viewers should possess a solid understanding of the target language. 0.8 - Advanced: The video incorporates sophisticated vocabulary and intricate sentence structures. It may involve nuanced discussions that demand a high level of proficiency in the language. 1 - Very Advanced: This level targets proficient speakers, featuring specialized terms and concepts that might be unfamiliar even to native speakers. User: Description: These are some email starters in Spanish! Of course there are more, and they have different formality levels, which is why it's so hard to know which to use in certain situations, like in the video! Hola ➡️ Hello. Buenos días ➡️Good morning. Buenas tardes ➡️Good afternoon. Hola, buenas tardes ➡️ Hello, good afternoon. Hola, buenas ➡️ Hello, how is it going? Querido ➡️Dear. Queridísimo ➡️Dearest. Estimado ➡️Esteemed. Estimada ➡️Esteemed (female). Saludos ➡️Greetings. A quién le corresponda este email ➡️To whom it may concern. BOOKS: (These are affiliate links, they help me get my campervan! If you purchase a product or service with the links that I provide I may receive a small commission. There is no additional charge to you!) Short Stories in Spanish (BEGINNERS): https://amzn.to/3Bpovgb Short Stories in Spanish (INTERMEDIATE): https://amzn.to/3yt5hEn Other Languages: Short Stories in English (BEGINNERS): https://amzn.to/3gMJ8ed Short Stories in English (INTERMEDIATE): https://amzn.to/2WD0uTA Short Stories in French (BEGINNERS): https://amzn.to/3yuVWvZ Short Stories in French (INTERMEDIATE): https://amzn.to/3t2Ggid VIDEOS: For BEGINNERS! Making An Appointment With A Doctor In Spanish: https://youtu.be/Sfs-lQnV0Yg My Present To You, 5 MINUTE BROWNIE: https://youtu.be/INwJuiFn500 ADVENT CALENDAR: https://youtu.be/p2q3BZE8Uro Puzzle Time! EASY SPANISH LISTENING PRACTICE FOR BEGINNERS!: https://youtu.be/NWDv6nQ3ssQ INTERMEDIATE! Learn Spanish Only To Crush It With Your Own Hands // SPANISH TEXTING: https://youtu.be/EqRC9POqU84 I bought a ball for the sake of my back: https://youtu.be/Pj1PWAS0Imw Come with me to the BEACH and learn some SPANISH while you are at it: https://youtu.be/8kFXlmsDpKI Come learn more on INSTAGRAM! Or just say hi -https://www.instagram.com/spanishafterhours/ SUBSCRIBE FOR MORE SPANISH AND FUN!! :) Title: starting emails in Spanish be like: Language: es Caption: hola es muy simple buenos días o buenas tardes no sé que habrá será cuando lo abran a ver y si convino hola buenas tardes y escribo hola buenas tardes quedaría bien y sigo teniendo el problema de la hora no me vale hola buenas no esto es demasiado informal y si escribo algo como querido queridísimo que logra que estás diciendo a ver ya sé estimado o estimado estimado o estimada normalmente cuando escribo me contesta una mujer pero y si esta vez me contesta un hombre esto tampoco sirve nada [Música] saludos esto tampoco funciona sabes qué a quien le corresponda este y Assistant:
```json
{
"result": 0.3
}
```0cae16be236a130b043f5116System: You are a JSON assistant. You only reply in valid JSON and never in normal text. Provide a difficulty rating for the video information detailed below. I would like a single property named "result" that contains a float value between 0 and 1. The difficulty score categories are as follows: 0.0 - Absolute Beginner: This level features extremely simple language and basic expressions, accompanied by clear visuals and context. It is perfect for individuals with no knowledge of the target language. 0.3 - Beginner: This level includes straightforward sentences and commonly used words. The video may feature some visual support and context to aid comprehension. 0.5 - Intermediate: This level presents more intricate sentences and a wider range of vocabulary. Some idiomatic phrases may be included, necessitating a bit more background understanding. 0.6 - Upper Intermediate: This level contains specialized vocabulary and concepts related to specific fields. Viewers should possess a solid understanding of the target language for complete comprehension. 0.8 - Advanced: This level incorporates sophisticated vocabulary and intricate sentence structures. It may present nuanced topics that demand a high level of language proficiency. 1 - Very Advanced: This level targets fluent individuals, featuring specialized language and concepts that may not be widely recognized by all native speakers. Assistant: Description: These are some email starters in Spanish! Of course there are more, and they have different formality levels, which is why it's so hard to know which to use in certain situations, like in the video! Hola ➡️ Hello. Buenos días ➡️Good morning. Buenas tardes ➡️Good afternoon. Hola, buenas tardes ➡️ Hello, good afternoon. Hola, buenas ➡️ Hello, how is it going? Querido ➡️Dear. Queridísimo ➡️Dearest. Estimado ➡️Esteemed. Estimada ➡️Esteemed (female). Saludos ➡️Greetings. A quién le corresponda este email ➡️To whom it may concern. BOOKS: (These are affiliate links, they help me get my campervan! If you purchase a product or service with the links that I provide I may receive a small commission. There is no additional charge to you!) Short Stories in Spanish (BEGINNERS): https://amzn.to/3Bpovgb Short Stories in Spanish (INTERMEDIATE): https://amzn.to/3yt5hEn Other Languages: Short Stories in English (BEGINNERS): https://amzn.to/3gMJ8ed Short Stories in English (INTERMEDIATE): https://amzn.to/2WD0uTA Short Stories in French (BEGINNERS): https://amzn.to/3yuVWvZ Short Stories in French (INTERMEDIATE): https://amzn.to/3t2Ggid VIDEOS: For BEGINNERS! Making An Appointment With A Doctor In Spanish: https://youtu.be/Sfs-lQnV0Yg My Present To You, 5 MINUTE BROWNIE: https://youtu.be/INwJuiFn500 ADVENT CALENDAR: https://youtu.be/p2q3BZE8Uro Puzzle Time! EASY SPANISH LISTENING PRACTICE FOR BEGINNERS!: https://youtu.be/NWDv6nQ3ssQ INTERMEDIATE! Learn Spanish Only To Crush It With Your Own Hands // SPANISH TEXTING: https://youtu.be/EqRC9POqU84 I bought a ball for the sake of my back: https://youtu.be/Pj1PWAS0Imw Come with me to the BEACH and learn some SPANISH while you are at it: https://youtu.be/8kFXlmsDpKI Come learn more on INSTAGRAM! Or just say hi -https://www.instagram.com/spanishafterhours/ SUBSCRIBE FOR MORE SPANISH AND FUN!! :) Language: es Caption: hola es muy simple buenos días o buenas tardes no sé que habrá será cuando lo abran a ver y si convino hola buenas tardes y escribo hola buenas tardes quedaría bien y sigo teniendo el problema de la hora no me vale hola buenas no esto es demasiado informal y si escribo algo como querido queridísimo que logra que estás diciendo a ver ya sé estimado o estimado estimado o estimada normalmente cuando escribo me contesta una mujer pero y si esta vez me contesta un hombre esto tampoco sirve nada [Música] saludos esto tampoco funciona sabes qué a quien le corresponda este y Title: starting emails in Spanish be like: Assistant:
```json
{
"result": 0.3
}
```a5e18fafde4c161d45b83f4c# components/run_backtest.py
import streamlit as st
import pandas as pd
import numpy as np
import plotly.express as px
# Importing all necessary functions from utils, metrics, and plots
from utils import (
download_data,
generate_recommendations,
backtest,
fetch_fama_french_factors,
)
from metrics import (
calculate_portfolio_metrics,
calculate_final_score,
calculate_risk_factor_attribution,
get_drawdown_details
)
from plots import (
plot_growth_comparison,
plot_drawdown_comparison,
plot_cagr_over_time,
plot_allocation_pie,
plot_correlation_heatmap,
plot_risk_return_attribution,
plot_rolling_metrics,
plot_correlation_heatmap_comparison,
plot_cagr_comparison,
plot_allocation_comparison,
plot_rolling_metrics_comparison,
plot_risk_return_attribution_comparison,
plot_efficient_frontier_comparison
)
def run_backtest_ui():
# Initialize portfolios in session state if not already initialized
if 'portfolios' not in st.session_state:
st.session_state.portfolios = []
if st.session_state.portfolios:
with st.form("compare_form"):
st.subheader("🔍 Select Portfolios to Compare")
comparison_type = st.selectbox(
"Comparison Type",
["Portfolio vs Benchmark", "Portfolio vs Portfolio"]
)
portfolio_names = [p['name'] for p in st.session_state.portfolios]
if comparison_type == "Portfolio vs Benchmark":
portfolio1 = st.selectbox("Select Portfolio", portfolio_names)
portfolio2 = None
else:
portfolio1 = st.selectbox("Select First Portfolio", portfolio_names, key="p1")
portfolio2 = st.selectbox("Select Second Portfolio", portfolio_names, key="p2")
if portfolio1 == portfolio2:
st.warning("Please select two different portfolios for comparison.")
st.subheader("⚙️ Visualizations Settings")
selected_time_frames = st.multiselect(
"Select Time Frames for CAGR",
options=['Weekly', 'Monthly', 'Quarterly', 'Annually'],
default=['Annually'],
key="selected_time_frames",
help="Choose one or more time frames to view CAGR over different horizons."
)
selected_rolling_periods = st.multiselect(
"Select Rolling Periods (Days)",
options=[30, 90, 180, 252],
default=[252],
key="selected_rolling_periods",
help="Choose one or more periods to view rolling metrics."
)
run = st.form_submit_button("Run Backtest")
if run:
with st.spinner("Running backtest..."):
if comparison_type == "Portfolio vs Benchmark":
# Handle Portfolio vs Benchmark
try:
portfolio = next(p for p in st.session_state.portfolios if p['name'] == portfolio1)
except StopIteration:
st.error("Selected portfolio not found.")
return
price_data = download_data(
portfolio['selected'],
portfolio['start_date'],
portfolio['end_date']
)
if price_data.empty:
st.error("No price data available for the selected portfolio.")
else:
available_selected = [ticker for ticker in portfolio['selected'] if ticker in price_data.columns]
missing_selected = list(set(portfolio['selected']) - set(available_selected))
if missing_selected:
st.warning(f"Excluded tickers with no data: {', '.join(missing_selected)}")
if not available_selected:
st.error(
"No selected tickers have available data for the chosen date range. "
"Please ensure that the ticker symbols are correct and data is available for the specified period."
)
else:
# Extract corresponding allocations
original_allocations = portfolio.get('allocations', [0.0]*len(portfolio.get('selected', [])))
adjusted_allocations = [original_allocations[i] for i, ticker in enumerate(portfolio.get('selected', [])) if ticker in available_selected]
# Error Handling for Allocation-Axis
if len(adjusted_allocations) != len(available_selected):
st.error("Mismatch between allocations and available assets after filtering.")
return
# Normalize allocations to sum to 1
weights = np.array(adjusted_allocations) / 100
total_alloc = weights.sum()
if not np.isclose(total_alloc, 1.0, atol=1e-4):
if total_alloc == 0:
st.error("Total allocations sum to 0. Cannot adjust allocations.")
return
st.warning("Allocations do not sum to 100%. Adjusting allocations proportionally.")
weights = weights / total_alloc
# Final check to ensure weights match the number of assets
if len(weights) != len(available_selected):
st.error("Final allocation weights do not match the number of available assets.")
return
# Calculate per-asset returns
asset_returns = price_data[available_selected].pct_change().dropna()
# Run backtest to get portfolio returns
returns, cum_returns = backtest(
weights,
price_data[available_selected],
portfolio['rebalance_freq'],
portfolio['broker_fee'],
debug=False
)
# Align asset returns with portfolio returns
common_index = returns.index.intersection(asset_returns.index)
asset_returns = asset_returns.loc[common_index]
returns = returns.loc[common_index]
cum_returns = cum_returns.loc[common_index]
if cum_returns.empty:
st.error("Cumulative returns are empty. Check the data and allocations.")
else:
benchmark_symbol = portfolio['benchmark_symbol']
benchmark_data = download_data([benchmark_symbol], portfolio['start_date'], portfolio['end_date'])
if benchmark_symbol not in benchmark_data.columns:
st.error(f"The selected benchmark symbol '{benchmark_symbol}' does not have available data for the chosen period.")
st.stop()
else:
st.success(f"Benchmark '{benchmark_symbol}' data successfully downloaded and will be used for comparison.")
if not benchmark_data.empty:
benchmark_returns = benchmark_data[benchmark_symbol].pct_change().dropna()
benchmark_returns = benchmark_returns.reindex(returns.index, method='ffill').dropna()
common_index_benchmark = returns.index.intersection(benchmark_returns.index)
if common_index_benchmark.empty:
st.error("No overlapping dates between portfolio returns and benchmark returns after alignment.")
st.stop()
returns = returns.loc[common_index_benchmark]
cum_returns = cum_returns.loc[common_index_benchmark]
asset_returns = asset_returns.loc[common_index_benchmark]
benchmark_returns = benchmark_returns.loc[common_index_benchmark]
benchmark_cum_returns = (1 + benchmark_returns).cumprod()
benchmark_metrics = calculate_portfolio_metrics(
benchmark_returns, benchmark_cum_returns, portfolio['rf_rate'], None
)
else:
benchmark_returns = None
benchmark_cum_returns = pd.Series(dtype=float) # Ensure it's defined
benchmark_metrics = {
'Start Balance': "N/A",
'End Balance': "N/A",
'Annualized Return (CAGR)': "N/A",
'Best Year': "N/A",
'Worst Year': "N/A",
'Arithmetic Mean (Monthly)': "N/A",
'Arithmetic Mean (Annualized)': "N/A",
'Geometric Mean (Monthly)': "N/A",
'Geometric Mean (Annualized)': "N/A",
'Standard Deviation (Monthly)': "N/A",
'Standard Deviation (Annualized)': "N/A",
'Downside Deviation (Monthly)': "N/A",
'Maximum Drawdown': "N/A",
'Sharpe Ratio': "N/A",
'Sortino Ratio': "N/A",
'Gain/Loss Ratio': "N/A",
'Skewness': "N/A",
'Excess Kurtosis': "N/A",
'Safe Withdrawal Rate': "N/A",
'Perpetual Withdrawal Rate': "N/A",
'Positive Periods': "N/A",
'Benchmark Correlation': np.nan,
'Beta': np.nan,
'R2': np.nan
}
portfolio_metrics = calculate_portfolio_metrics(
returns, cum_returns, portfolio['rf_rate'], benchmark_returns
)
# Dashboard Header with Key Stats
with st.container():
st.markdown("### 🔑 Key Metrics")
key_metrics = {
'Annualized Return (CAGR)': portfolio_metrics.get('Annualized Return (CAGR)', "N/A"),
'Sharpe Ratio': portfolio_metrics.get('Sharpe Ratio', "N/A"),
'Maximum Drawdown': portfolio_metrics.get('Maximum Drawdown', "N/A")
}
cols = st.columns(len(key_metrics))
for col, (metric, value) in zip(cols, key_metrics.items()):
with col:
st.metric(label=metric, value=value)
# Create Tabs for Organized Sections
tabs = st.tabs(["Overview", "Performance Statistics", "Advanced Metrics", "Drawdowns", "Visualizations"])
with tabs[0]:
st.header("📈 Portfolio Performance Overview")
st.write(f"**Portfolio Name:** {portfolio['name']}")
st.write(f"**Start Date:** {portfolio['start_date'].strftime('%Y-%m-%d')}")
st.write(f"**End Date:** {portfolio['end_date'].strftime('%Y-%m-%d')}")
st.write(f"**Benchmark:** {portfolio['benchmark_symbol']}")
with tabs[1]:
st.header("📊 Performance Statistics")
performance_data_stats = {
'Metric': [
'Start Balance',
'End Balance',
'Annualized Return (CAGR)',
'Standard Deviation (Annualized)',
'Best Year',
'Worst Year',
'Maximum Drawdown',
'Sharpe Ratio',
'Sortino Ratio',
'Benchmark Correlation'
],
'Portfolio': [
portfolio_metrics['Start Balance'],
portfolio_metrics['End Balance'],
f"{portfolio_metrics['Annualized Return (CAGR)']:.2f}%" if not np.isnan(portfolio_metrics['Annualized Return (CAGR)']) else "N/A",
f"{portfolio_metrics['Standard Deviation (Annualized)']:.2f}%" if not np.isnan(portfolio_metrics['Standard Deviation (Annualized)']) else "N/A",
f"{portfolio_metrics['Best Year']:.2f}%" if not np.isnan(portfolio_metrics['Best Year']) else "N/A",
f"{portfolio_metrics['Worst Year']:.2f}%" if not np.isnan(portfolio_metrics['Worst Year']) else "N/A",
f"{portfolio_metrics['Maximum Drawdown']:.2f}%" if not np.isnan(portfolio_metrics['Maximum Drawdown']) else "N/A",
f"{portfolio_metrics['Sharpe Ratio']:.2f}" if not np.isnan(portfolio_metrics['Sharpe Ratio']) else "N/A",
f"{portfolio_metrics['Sortino Ratio']:.2f}" if not np.isnan(portfolio_metrics['Sortino Ratio']) else "N/A",
f"{portfolio_metrics['Benchmark Correlation']:.2f}" if not np.isnan(portfolio_metrics['Benchmark Correlation']) else "N/A"
],
'Benchmark': [
benchmark_metrics.get('Start Balance', "N/A"),
benchmark_metrics.get('End Balance', "N/A"),
f"{benchmark_metrics.get('Annualized Return (CAGR)', np.nan):.2f}%" if not pd.isna(benchmark_metrics.get('Annualized Return (CAGR)', np.nan)) else "N/A",
f"{benchmark_metrics.get('Standard Deviation (Annualized)', np.nan):.2f}%" if not pd.isna(benchmark_metrics.get('Standard Deviation (Annualized)', np.nan)) else "N/A",
f"{benchmark_metrics.get('Best Year', np.nan):.2f}%" if not pd.isna(benchmark_metrics.get('Best Year', np.nan)) else "N/A",
f"{benchmark_metrics.get('Worst Year', np.nan):.2f}%" if not pd.isna(benchmark_metrics.get('Worst Year', np.nan)) else "N/A",
f"{benchmark_metrics.get('Maximum Drawdown', np.nan):.2f}%" if not pd.isna(benchmark_metrics.get('Maximum Drawdown', np.nan)) else "N/A",
f"{benchmark_metrics.get('Sharpe Ratio', np.nan):.2f}" if not pd.isna(benchmark_metrics.get('Sharpe Ratio', np.nan)) else "N/A",
f"{benchmark_metrics.get('Sortino Ratio', np.nan):.2f}" if not pd.isna(benchmark_metrics.get('Sortino Ratio', np.nan)) else "N/A",
f"{benchmark_metrics.get('Benchmark Correlation', np.nan):.2f}" if not pd.isna(benchmark_metrics.get('Benchmark Correlation', np.nan)) else "N/A"
]
}
performance_df_stats = pd.DataFrame(performance_data_stats).set_index('Metric')
st.table(performance_df_stats)
with tabs[2]:
st.header("📋 Detail Comparisons")
with st.container():
st.subheader("🧮 Advanced Metrics")
performance_data_advanced = {
'Metric': list(portfolio_metrics.keys()),
'Portfolio': list(portfolio_metrics.values()),
'Benchmark': list(benchmark_metrics.values())
}
performance_df_advanced = pd.DataFrame(performance_data_advanced).set_index('Metric')
# Replace NaN with empty strings for better visualization
performance_df_advanced = performance_df_advanced.replace(np.nan, "")
# Drop rows where both Portfolio and Benchmark values are NaN
performance_df_advanced_clean = performance_df_advanced.dropna(how='all')
# Format metrics to two decimal places
performance_df_advanced_clean = performance_df_advanced_clean.applymap(format_value)
st.table(performance_df_advanced_clean)
st.markdown("---")
st.write("**Note:** Some metrics in the benchmark column are empty because they are portfolio-specific measurements that can't be calculated for the benchmark alone...")
st.subheader("🔍 Risk Factor Attribution Analysis")
# Define date range based on the overlapping index
start_date = returns.index.min().strftime('%Y-%m-%d')
end_date = returns.index.max().strftime('%Y-%m-%d')
# Fetch actual factor data
factors = fetch_fama_french_factors(start_date, end_date)
# Ensure factors are fetched
if factors.empty:
st.write("Failed to retrieve factor data.")
else:
# Align factors frequency with portfolios
if isinstance(returns.index, pd.DatetimeIndex):
if returns.index.freq is None:
inferred_freq = pd.infer_freq(returns.index)
if inferred_freq is None:
inferred_freq = 'D' # Set default frequency to daily
else:
inferred_freq = returns.index.freq
try:
factors = factors.asfreq(inferred_freq, method='ffill')
except TypeError as e:
st.error(f"Error setting frequency for factors: {e}")
st.stop()
else:
st.error("Returns index is not a DatetimeIndex.")
st.stop()
# Calculate Attribution for Portfolio
attribution, error = calculate_risk_factor_attribution(returns, factors)
if error:
st.error("Risk factor attribution analysis is unavailable for the portfolio.")
else:
if attribution.empty:
st.write("Risk factor attribution analysis is unavailable for the portfolio.")
else:
st.write(f"### Risk Factor Attribution for {portfolio['name']}")
st.table(attribution.fillna(0))
# Visualization
fig_attribution = px.bar(
attribution,
x='Factor',
y='Contribution (%)',
title='Risk Factor Attribution',
template='plotly_dark'
)
st.plotly_chart(fig_attribution, use_container_width=True)
with tabs[3]:
st.header("📉 Detailed Drawdowns")
st.markdown("### 📈 Drawdowns for Portfolio")
portfolio_drawdowns = get_drawdown_details(cum_returns)
if portfolio_drawdowns:
portfolio_drawdowns_df = pd.DataFrame(portfolio_drawdowns)
st.table(portfolio_drawdowns_df)
else:
st.write("No drawdowns detected for the portfolio.")
st.markdown("### 📈 Drawdowns for Benchmark")
benchmark_drawdowns = get_drawdown_details(benchmark_cum_returns)
if benchmark_drawdowns:
benchmark_drawdowns_df = pd.DataFrame(benchmark_drawdowns)
st.table(benchmark_drawdowns_df)
else:
st.write("No drawdowns detected for the benchmark.")
with tabs[4]:
st.header("📊 Visualizations")
if not cum_returns.empty and not benchmark_cum_returns.empty:
plot_growth_comparison(cum_returns, benchmark_cum_returns, labels=["Portfolio", "Benchmark"])
else:
st.warning("Insufficient data to display Growth Comparison. Ensure both portfolio and benchmark have data.")
portfolio_drawdown_series = (cum_returns / cum_returns.expanding().max() - 1) * 100
benchmark_drawdown_series = (benchmark_cum_returns / benchmark_cum_returns.expanding().max() - 1) * 100
plot_drawdown_comparison(portfolio_drawdown_series, benchmark_drawdown_series, labels=["Portfolio", "Benchmark"])
st.subheader("📈 Compound Annual Growth Rate (CAGR) Over Time")
if selected_time_frames:
plot_cagr_over_time(cum_returns, time_frames=selected_time_frames, label="Portfolio")
else:
st.warning("Please select at least one time frame for CAGR.")
# Box Plot for Returns Distribution
st.subheader("📦 Returns Distribution Box Plot")
if isinstance(asset_returns, pd.DataFrame):
# Assuming the index is named 'Date'
if asset_returns.index.name != 'Date':
asset_returns = asset_returns.copy()
asset_returns.index.name = 'Date'
melted_returns = asset_returns.reset_index().melt(id_vars='Date', var_name='Asset', value_name='Return')
fig_box_plot = px.box(
melted_returns,
x='Asset',
y='Return',
title='Returns Distribution Box Plot',
labels={'Return': 'Returns', 'Asset': 'Asset'},
points='all',
hover_data=['Return'],
template='plotly_dark',
color='Asset',
color_discrete_sequence=px.colors.qualitative.Set3
)
st.plotly_chart(fig_box_plot, use_container_width=True)
st.markdown("**Interpretation:** The box plot visualizes the distribution of returns for each asset in the portfolio. The boxes represent the interquartile range (IQR), the line inside the box indicates the median, and the whiskers show the range of the data. Outliers are displayed as individual points.")
elif isinstance(asset_returns, pd.Series):
# Assuming the index is named 'Date'
if asset_returns.index.name != 'Date':
asset_returns = asset_returns.copy()
asset_returns.index.name = 'Date'
melted_returns = asset_returns.reset_index().rename(columns={'Date': 'Date', 0: 'Return'})
fig_box_plot = px.box(
melted_returns,
y='Return',
title='Returns Distribution Box Plot',
labels={'Return': 'Returns'},
points='all',
hover_data=['Return'],
template='plotly_dark'
)
st.plotly_chart(fig_box_plot, use_container_width=True)
st.markdown("**Interpretation:** The box plot visualizes the distribution of returns for the portfolio. The box represents the interquartile range (IQR), the line inside the box indicates the median, and the whiskers show the range of the data. Outliers are displayed as individual points.")
else:
st.warning("Returns data is neither a DataFrame nor a Series.")
# Heatmap of Correlations Between Assets
st.subheader("🔥 Correlation Heatmap of Assets")
plot_correlation_heatmap(asset_returns)
# Add Risk-Return Attribution Analysis
st.subheader("🔍 Risk-Return Attribution Analysis")
plot_risk_return_attribution(returns, weights, label='Portfolio')
# Cumulative Returns Heatmap
st.subheader("🔥 Cumulative Returns Heatmap")
try:
cum_returns_normalized = cum_returns / cum_returns.expanding().max()
fig_cum_heatmap = px.imshow(
cum_returns_normalized.to_frame().T,
labels=dict(x="Date", y="Portfolio", color="Normalized Cumulative Return"),
title="Cumulative Returns Heatmap",
aspect="auto",
color_continuous_scale='Viridis',
template='plotly_dark'
)
st.plotly_chart(fig_cum_heatmap, use_container_width=True)
st.markdown("**Interpretation:** The heatmap visualizes the normalized cumulative returns over time, allowing for an intuitive comparison of portfolio performance across different periods.")
except Exception as e:
st.error(f"Error plotting cumulative returns heatmap: {e}")
# Portfolio Allocation Pie Chart
st.subheader("🥧 Portfolio Allocation Pie Chart")
plot_allocation_pie(weights, available_selected, title="Portfolio Allocation", hover_info="percent+name")
# Rolling Metrics Chart with User-Defined Periods
st.subheader("📉 Rolling Metrics")
if selected_rolling_periods:
plot_rolling_metrics(returns, windows=selected_rolling_periods, label='Portfolio')
else:
st.warning("Please select at least one rolling period to display metrics.")
# Recommendations
st.markdown("### 💡 Recommendations")
recommendations = generate_recommendations(
allocations=[alloc for alloc in portfolio['allocations'] if alloc > 0],
available_selected=[ticker for ticker in available_selected if portfolio['allocations'][available_selected.index(ticker)] > 0]
)
if recommendations:
for rec in recommendations:
st.write(rec)
else:
st.success("Your portfolio allocations are well-balanced!")
# Final Score
st.markdown("### 📝 Final Score")
skipped_metrics_set = set()
portfolio_score = calculate_final_score(portfolio_metrics, benchmark_metrics, global_skipped=skipped_metrics_set)
benchmark_score = calculate_final_score(benchmark_metrics, portfolio_metrics, global_skipped=skipped_metrics_set)
st.write(f"**Portfolio:** {portfolio_score:.2f} / 100")
st.write(f"**Benchmark:** {benchmark_score:.2f} / 100")
# Show skipped metrics once
if skipped_metrics_set:
st.warning(f"Skipped metrics due to invalid values: {', '.join(skipped_metrics_set)}")
# Store results
st.session_state.backtest_results = {
'returns': returns,
'cum_returns': cum_returns,
'weights_a': weights,
'price_data': price_data[available_selected],
'metrics': portfolio_metrics,
'benchmark_metrics': benchmark_metrics,
'performance_df_stats': performance_df_stats_formatted,
'performance_df_advanced': performance_df_advanced_clean, # Corrected to use the cleaned DataFrame
'benchmark_cum_returns': benchmark_cum_returns
}
elif comparison_type == "Portfolio vs Portfolio" and portfolio1 and portfolio2 and portfolio1 != portfolio2:
# Handle Portfolio vs Portfolio
try:
portfolio_a = next(p for p in st.session_state.portfolios if p['name'] == portfolio1)
portfolio_b = next(p for p in st.session_state.portfolios if p['name'] == portfolio2)
except StopIteration:
st.error("One or both selected portfolios were not found.")
return
# Initialize cum_returns_a and cum_returns_b to prevent UnboundLocalError
cum_returns_a = pd.Series(dtype=float)
cum_returns_b = pd.Series(dtype=float)
# Backtest Portfolio A
price_data_a = download_data(
portfolio_a['selected'],
portfolio_a['start_date'],
portfolio_a['end_date']
)
# Backtest Portfolio B
price_data_b = download_data(
portfolio_b['selected'],
portfolio_b['start_date'],
portfolio_b['end_date']
)
if price_data_a.empty or price_data_b.empty:
st.error("No price data available for one or both selected portfolios.")
else:
# Process Portfolio A
available_a = [ticker for ticker in portfolio_a['selected'] if ticker in price_data_a.columns]
missing_a = list(set(portfolio_a['selected']) - set(available_a))
if missing_a:
st.warning(f"Excluded tickers from {portfolio_a['name']} with no data: {', '.join(missing_a)}")
if not available_a:
st.error(f"No selected tickers have available data for the portfolio '{portfolio_a['name']}'.")
return
# Extract corresponding allocations for Portfolio A
original_allocations_a = portfolio_a.get('allocations', [0.0]*len(portfolio_a.get('selected', [])))
adjusted_allocations_a = [original_allocations_a[i] for i, ticker in enumerate(portfolio_a.get('selected', [])) if ticker in available_a]
if len(adjusted_allocations_a) != len(available_a):
st.error(f"Mismatch between allocations and available assets after filtering for '{portfolio_a['name']}'.")
return
# Normalize allocations to sum to 1
weights_a = np.array(adjusted_allocations_a) / 100
total_alloc_a = weights_a.sum()
if not np.isclose(total_alloc_a, 1.0, atol=1e-4):
if total_alloc_a == 0:
st.error(f"Total allocations for '{portfolio_a['name']}' sum to 0. Cannot adjust allocations.")
return
st.warning(f"Allocations for '{portfolio_a['name']}' do not sum to 100%. Adjusting allocations proportionally.")
weights_a = weights_a / total_alloc_a
# Calculate per-asset returns for Portfolio A
asset_returns_a = price_data_a[available_a].pct_change().dropna()
# Run backtest for Portfolio A
returns_a, cum_returns_a = backtest(
weights_a,
price_data_a[available_a],
portfolio_a['rebalance_freq'],
portfolio_a['broker_fee'],
debug=False
)
# Process Portfolio B
available_b = [ticker for ticker in portfolio_b['selected'] if ticker in price_data_b.columns]
missing_b = list(set(portfolio_b['selected']) - set(available_b))
if missing_b:
st.warning(f"Excluded tickers from {portfolio_b['name']} with no data: {', '.join(missing_b)}")
if not available_b:
st.error(f"No selected tickers have available data for the portfolio '{portfolio_b['name']}'.")
return
# Extract corresponding allocations for Portfolio B
original_allocations_b = portfolio_b.get('allocations', [0.0]*len(portfolio_b.get('selected', [])))
adjusted_allocations_b = [original_allocations_b[i] for i, ticker in enumerate(portfolio_b.get('selected', [])) if ticker in available_b]
if len(adjusted_allocations_b) != len(available_b):
st.error(f"Mismatch between allocations and available assets after filtering for '{portfolio_b['name']}'.")
return
# Normalize allocations to sum to 1
weights_b = np.array(adjusted_allocations_b) / 100
total_alloc_b = weights_b.sum()
if not np.isclose(total_alloc_b, 1.0, atol=1e-4):
if total_alloc_b == 0:
st.error(f"Total allocations for '{portfolio_b['name']}' sum to 0. Cannot adjust allocations.")
return
st.warning(f"Allocations for '{portfolio_b['name']}' do not sum to 100%. Adjusting allocations proportionally.")
weights_b = weights_b / total_alloc_b
# Calculate per-asset returns for Portfolio B
asset_returns_b = price_data_b[available_b].pct_change().dropna()
# Run backtest for Portfolio B
returns_b, cum_returns_b = backtest(
weights_b,
price_data_b[available_b],
portfolio_b['rebalance_freq'],
portfolio_b['broker_fee'],
debug=False
)
# Align the data for comparison
common_dates = returns_a.index.intersection(returns_b.index)
if common_dates.empty:
st.error("No overlapping dates between the two portfolios after alignment.")
return
cum_returns_a = cum_returns_a.loc[common_dates]
cum_returns_b = cum_returns_b.loc[common_dates]
returns_a = returns_a.loc[common_dates]
returns_b = returns_b.loc[common_dates]
asset_returns_a = asset_returns_a.loc[common_dates]
asset_returns_b = asset_returns_b.loc[common_dates]
# Calculate Metrics
metrics_a = calculate_portfolio_metrics(
returns_a, cum_returns_a, portfolio_a['rf_rate'], None
)
metrics_b = calculate_portfolio_metrics(
returns_b, cum_returns_b, portfolio_b['rf_rate'], None
)
# Dashboard Header with Key Stats
with st.container():
st.markdown("### 🔑 Key Metrics")
key_metrics = {
'Annualized Return (CAGR)': ('CAGR', metrics_a.get('Annualized Return (CAGR)', "N/A"), metrics_b.get('Annualized Return (CAGR)', "N/A")),
'Sharpe Ratio': ('Sharpe Ratio', metrics_a.get('Sharpe Ratio', "N/A"), metrics_b.get('Sharpe Ratio', "N/A")),
'Maximum Drawdown': ('Maximum Drawdown', metrics_a.get('Maximum Drawdown', "N/A"), metrics_b.get('Maximum Drawdown', "N/A"))
}
columns = st.columns(len(key_metrics))
for idx, (metric, (label, value_a, value_b)) in enumerate(key_metrics.items()):
with columns[idx]:
st.metric(label=f"{label} ({portfolio_a['name']})", value=value_a)
st.metric(label=f"{label} ({portfolio_b['name']})", value=value_b)
# Create Tabs for Organized Sections
tabs = st.tabs(["Overview", "Performance Statistics", "Advanced Metrics", "Drawdowns", "Visualizations"])
with tabs[0]:
st.header("📈 Portfolios Performance Overview")
# Determine the aligned start and end dates
aligned_start_date = common_dates.min().strftime('%Y-%m-%d')
aligned_end_date = common_dates.max().strftime('%Y-%m-%d')
st.write(f"**Aligned Date Range:** {aligned_start_date} to {aligned_end_date}")
st.write(f"**Portfolio A:** {portfolio_a['name']}")
st.write(f"**Original Start Date:** {portfolio_a['start_date'].strftime('%Y-%m-%d')}")
st.write(f"**Portfolio B:** {portfolio_b['name']}")
st.write(f"**Original Start Date:** {portfolio_b['start_date'].strftime('%Y-%m-%d')}")
with tabs[1]:
st.header("📊 Performance Statistics")
with st.container():
st.subheader("🛠️ Basic Metrics")
performance_data_stats = {
'Metric': [
'Start Balance',
'End Balance',
'Annualized Return (CAGR)',
'Standard Deviation (Annualized)',
'Best Year',
'Worst Year',
'Maximum Drawdown',
'Sharpe Ratio',
'Sortino Ratio',
'Benchmark Correlation'
],
f'{portfolio_a["name"]}': [
metrics_a['Start Balance'],
metrics_a['End Balance'],
metrics_a['Annualized Return (CAGR)'],
metrics_a['Standard Deviation (Annualized)'],
metrics_a['Best Year'],
metrics_a['Worst Year'],
metrics_a['Maximum Drawdown'],
metrics_a['Sharpe Ratio'],
metrics_a['Sortino Ratio'],
metrics_a['Benchmark Correlation']
],
f'{portfolio_b["name"]}': [
metrics_b['Start Balance'],
metrics_b['End Balance'],
metrics_b['Annualized Return (CAGR)'],
metrics_b['Standard Deviation (Annualized)'],
metrics_b['Best Year'],
metrics_b['Worst Year'],
metrics_b['Maximum Drawdown'],
metrics_b['Sharpe Ratio'],
metrics_b['Sortino Ratio'],
metrics_b['Benchmark Correlation']
]
}
performance_df_stats = pd.DataFrame(performance_data_stats).set_index('Metric')
st.table(performance_df_stats)
with tabs[2]:
st.header("📋 Detail Comparisons")
with st.container():
st.subheader("🧮 Advanced Metrics")
performance_data_advanced = {
'Metric': list(metrics_a.keys()),
f'{portfolio_a["name"]}': list(metrics_a.values()),
f'{portfolio_b["name"]}': list(metrics_b.values())
}
performance_df_advanced = pd.DataFrame(performance_data_advanced).set_index('Metric')
# Replace NaN with empty strings for better visualization
performance_df_advanced = performance_df_advanced.replace(np.nan, "")
# Drop rows where both Portfolio A and Portfolio B values are NaN
performance_df_advanced_clean = performance_df_advanced.dropna(how='all', subset=[f'{portfolio_a["name"]}', f'{portfolio_b["name"]}'])
st.table(performance_df_advanced_clean)
st.markdown("---")
st.write("**Note:** Some metrics may be specific to certain portfolios or may not be directly comparable. Ensure to interpret the metrics in the context of each portfolio's strategy and objectives.")
st.subheader("🔍 Risk Factor Attribution Analysis")
# Define date range based on the overlapping index
start_date = common_dates.min().strftime('%Y-%m-%d')
end_date = common_dates.max().strftime('%Y-%m-%d')
# Fetch actual factor data
factors = fetch_fama_french_factors(start_date, end_date)
# Ensure factors are fetched
if factors.empty:
st.write("Failed to retrieve factor data.")
else:
# Align factors frequency with portfolios
if isinstance(returns_a.index, pd.DatetimeIndex):
if returns_a.index.freq is None:
inferred_freq = pd.infer_freq(returns_a.index)
if inferred_freq is None:
inferred_freq = 'D' # Set default frequency to daily
else:
inferred_freq = returns_a.index.freq
try:
factors = factors.asfreq(inferred_freq, method='ffill')
except TypeError as e:
st.error(f"Error setting frequency for factors: {e}")
st.stop()
else:
st.error("Returns index is not a DatetimeIndex.")
st.stop()
# Calculate Attribution for Portfolio A
attribution_a, error_a = calculate_risk_factor_attribution(returns_a, factors)
# Calculate Attribution for Portfolio B
attribution_b, error_b = calculate_risk_factor_attribution(returns_b, factors)
if error_a or error_b:
st.error("Risk factor attribution analysis is unavailable for one or both portfolios.")
else:
if attribution_a.empty or attribution_b.empty:
st.write("Risk factor attribution analysis is unavailable for one or both portfolios.")
else:
st.write(f"**Risk Factor Attribution for {portfolio_a['name']}:**")
st.table(attribution_a)
fig_attribution_a = px.bar(
attribution_a,
x='Factor',
y='Contribution (%)',
title=f'Risk Factor Attribution for {portfolio_a["name"]}',
labels={'Contribution (%)': 'Contribution (%)'},
template='plotly_dark'
)
st.plotly_chart(fig_attribution_a, use_container_width=True)
st.write(f"**Risk Factor Attribution for {portfolio_b['name']}:**")
st.table(attribution_b)
fig_attribution_b = px.bar(
attribution_b,
x='Factor',
y='Contribution (%)',
title=f'Risk Factor Attribution for {portfolio_b["name"]}',
labels={'Contribution (%)': 'Contribution (%)'},
template='plotly_dark'
)
st.plotly_chart(fig_attribution_b, use_container_width=True)
# Combined Bar Chart for Comparison
combined_attribution = pd.concat([
attribution_a.set_index('Factor')['Contribution (%)'],
attribution_b.set_index('Factor')['Contribution (%)']
], axis=1)
combined_attribution.columns = [portfolio_a['name'], portfolio_b['name']]
combined_attribution = combined_attribution.fillna(0)
st.markdown(f"### 📊 Risk Factor Attribution Comparison")
fig_combined_attribution = px.bar(
combined_attribution,
x=combined_attribution.index,
y=combined_attribution.columns,
title='Risk Factor Attribution Comparison',
labels={'value': 'Contribution (%)', 'Factor': 'Risk Factor'},
template='plotly_dark',
barmode='group'
)
st.plotly_chart(fig_combined_attribution, use_container_width=True)
with tabs[3]:
st.header("📉 Detailed Drawdowns")
st.markdown("### 📈 Drawdowns for Portfolio A")
portfolio_drawdowns = get_drawdown_details(cum_returns_a)
if portfolio_drawdowns:
portfolio_drawdowns_df = pd.DataFrame(portfolio_drawdowns)
st.table(portfolio_drawdowns_df)
else:
st.write("No drawdowns detected for Portfolio A.")
st.markdown("### 📈 Drawdowns for Portfolio B")
portfolio_b_drawdowns = get_drawdown_details(cum_returns_b)
if portfolio_b_drawdowns:
portfolio_b_drawdowns_df = pd.DataFrame(portfolio_b_drawdowns)
st.table(portfolio_b_drawdowns_df)
else:
st.write("No drawdowns detected for Portfolio B.")
with tabs[4]:
st.header("📊 Visualizations")
if not cum_returns_a.empty and not cum_returns_b.empty:
plot_growth_comparison(cum_returns_a, cum_returns_b, labels=[portfolio_a['name'], portfolio_b['name']])
else:
st.warning("Insufficient data to display Growth Comparison. Ensure both portfolios have data.")
portfolio_a_drawdown_series = (cum_returns_a / cum_returns_a.expanding().max() - 1) * 100
portfolio_b_drawdown_series = (cum_returns_b / cum_returns_b.expanding().max() - 1) * 100
plot_drawdown_comparison(portfolio_a_drawdown_series, portfolio_b_drawdown_series, labels=[portfolio_a['name'], portfolio_b['name']])
st.subheader("📈 Compound Annual Growth Rate (CAGR) Over Time")
if selected_time_frames:
plot_cagr_comparison(cum_returns_a, cum_returns_b, time_frames=selected_time_frames)
else:
st.warning("Please select at least one time frame for CAGR.")
# Box Plot for Returns Distribution
st.subheader("📦 Returns Distribution Box Plot")
# For portfolio A
if isinstance(asset_returns_a, pd.DataFrame):
# Ensure the index is named 'Date'
if asset_returns_a.index.name != 'Date':
asset_returns_a = asset_returns_a.copy()
asset_returns_a.index.name = 'Date'
melted_returns_a = asset_returns_a.reset_index().melt(id_vars='Date', var_name='Asset', value_name='Return')
fig_box_plot_a = px.box(
melted_returns_a,
x='Asset',
y='Return',
title='Returns Distribution Box Plot - Portfolio A',
labels={'Return': 'Returns', 'Asset': 'Asset'},
points='all',
hover_data=['Return'],
template='plotly_dark',
color='Asset',
color_discrete_sequence=px.colors.qualitative.Set3
)
st.plotly_chart(fig_box_plot_a, use_container_width=True)
st.markdown("**Interpretation:** The box plot visualizes the distribution of returns for each asset in Portfolio A. The boxes represent the interquartile range (IQR), the line inside the box indicates the median, and the whiskers show the range of the data. Outliers are displayed as individual points.")
elif isinstance(asset_returns_a, pd.Series):
# Ensure the index is named 'Date'
if asset_returns_a.index.name != 'Date':
asset_returns_a = asset_returns_a.copy()
asset_returns_a.index.name = 'Date'
melted_returns_a = asset_returns_a.reset_index().rename(columns={'Date': 'Date', 'Return': 'Return'})
fig_box_plot_a = px.box(
melted_returns_a,
y='Return',
title='Returns Distribution Box Plot - Portfolio A',
labels={'Return': 'Returns'},
points='all',
hover_data=['Return'],
template='plotly_dark'
)
st.plotly_chart(fig_box_plot_a, use_container_width=True)
st.markdown("**Interpretation:** The box plot visualizes the distribution of returns for Portfolio A. The box represents the interquartile range (IQR), the line inside the box indicates the median, and the whiskers show the range of the data. Outliers are displayed as individual points.")
else:
st.warning("Returns data for Portfolio A is neither a DataFrame nor a Series.")
# For portfolio B
if isinstance(asset_returns_b, pd.DataFrame):
# Ensure the index is named 'Date'
if asset_returns_b.index.name != 'Date':
asset_returns_b = asset_returns_b.copy()
asset_returns_b.index.name = 'Date'
melted_returns_b = asset_returns_b.reset_index().melt(id_vars='Date', var_name='Asset', value_name='Return')
fig_box_plot_b = px.box(
melted_returns_b,
x='Asset',
y='Return',
title='Returns Distribution Box Plot - Portfolio B',
labels={'Return': 'Returns', 'Asset': 'Asset'},
points='all',
hover_data=['Return'],
template='plotly_dark',
color='Asset',
color_discrete_sequence=px.colors.qualitative.Set3
)
st.plotly_chart(fig_box_plot_b, use_container_width=True)
st.markdown("**Interpretation:** The box plot visualizes the distribution of returns for each asset in Portfolio B. The boxes represent the interquartile range (IQR), the line inside the box indicates the median, and the whiskers show the range of the data. Outliers are displayed as individual points.")
elif isinstance(asset_returns_b, pd.Series):
# Ensure the index is named 'Date'
if asset_returns_b.index.name != 'Date':
asset_returns_b = asset_returns_b.copy()
asset_returns_b.index.name = 'Date'
melted_returns_b = asset_returns_b.reset_index().rename(columns={'Date': 'Date', 'Return': 'Return'})
fig_box_plot_b = px.box(
melted_returns_b,
y='Return',
title='Returns Distribution Box Plot - Portfolio B',
labels={'Return': 'Returns'},
points='all',
hover_data=['Return'],
template='plotly_dark'
)
st.plotly_chart(fig_box_plot_b, use_container_width=True)
st.markdown("**Interpretation:** The box plot visualizes the distribution of returns for Portfolio B. The box represents the interquartile range (IQR), the line inside the box indicates the median, and the whiskers show the range of the data. Outliers are displayed as individual points.")
else:
st.warning("Returns data for Portfolio B is neither a DataFrame nor a Series.")
# Correlation Heatmap Comparison
st.subheader("🔥 Correlation Heatmap Comparison")
plot_correlation_heatmap_comparison(asset_returns_a, asset_returns_b)
# Efficient Frontier Comparison
st.subheader("📈 Efficient Frontier Comparison")
plot_efficient_frontier_comparison(asset_returns_a, asset_returns_b, num_portfolios=1000, rf=portfolio_a['rf_rate'])
# Risk-Return Attribution Comparison
st.subheader("🔍 Risk-Return Attribution Comparison")
plot_risk_return_attribution_comparison(
returns_a, weights_a,
returns_b, weights_b,
label_a=portfolio_a['name'],
label_b=portfolio_b['name']
)
# Recommendations
st.markdown("### 💡 Recommendations")
recommendations_a = generate_recommendations(
allocations=[alloc for alloc in portfolio_a['allocations'] if alloc > 0],
available_selected=[ticker for ticker in available_a if portfolio_a['allocations'][available_a.index(ticker)] > 0]
)
recommendations_b = generate_recommendations(
allocations=[alloc for alloc in portfolio_b['allocations'] if alloc > 0],
available_selected=[ticker for ticker in available_b if portfolio_b['allocations'][available_b.index(ticker)] > 0]
)
if recommendations_a:
st.write(f"**{portfolio_a['name']} Recommendations:**")
for rec in recommendations_a:
st.write(rec)
else:
st.success(f"{portfolio_a['name']} allocations are well-balanced!")
if recommendations_b:
st.write(f"**{portfolio_b['name']} Recommendations:**")
for rec in recommendations_b:
st.write(rec)
else:
st.success(f"{portfolio_b['name']} allocations are well-balanced!")
# Final Score
st.markdown("### 📝 Final Score")
portfolio_score_a = calculate_final_score(metrics_a, metrics_b)
portfolio_score_b = calculate_final_score(metrics_b, metrics_a)
st.write(f"**{portfolio_a['name']}:** {portfolio_score_a:.2f} / 100")
st.write(f"**{portfolio_b['name']}:** {portfolio_score_b:.2f} / 100")
# Store results
st.session_state.backtest_results = {
'returns_a': returns_a,
'cum_returns_a': cum_returns_a,
'weights_a': weights_a,
'price_data_a': price_data_a[available_a],
'metrics_a': metrics_a,
'returns_b': returns_b,
'cum_returns_b': cum_returns_b,
'weights_b': weights_b,
'price_data_b': price_data_b[available_b],
'metrics_b': metrics_b,
'performance_df_stats_a': performance_df_stats.loc[:, portfolio_a['name']],
'performance_df_advanced_a': performance_df_advanced_clean[[f'{portfolio_a["name"]}']],
'performance_df_stats_b': performance_df_stats.loc[:, portfolio_b['name']],
'performance_df_advanced_b': performance_df_advanced_clean[[f'{portfolio_b["name"]}']]
}
st.success("Backtest completed successfully!")
else:
st.warning("Please add at least one portfolio to run backtest.")
NameError: name 'format_value' is not defined
Traceback:
File "/www/server/pyporject_evn/portfolio_optimizer_venv/lib/python3.12/site-packages/streamlit/runtime/scriptrunner/exec_code.py", line 88, in exec_func_with_error_handling
result = func()
^^^^^^
File "/www/server/pyporject_evn/portfolio_optimizer_venv/lib/python3.12/site-packages/streamlit/runtime/scriptrunner/script_runner.py", line 579, in code_to_exec
exec(code, module.__dict__)
File "/www/wwwroot/portfolio_optimizer/pages/2_Run_Backtest.py", line 7, in <module>
run_backtest_ui()
File "/www/wwwroot/portfolio_optimizer/components/run_backtest.py", line 293, in run_backtest_ui
performance_df_advanced_clean = performance_df_advanced_clean.applymap(format_value)
^^^^^^^^^^^^
fix it and return full codeThe error you're encountering is due to the `format_value` function being used in your code without being defined anywhere. To resolve this issue, you need to define the `format_value` function before it's called. This function is typically used to format numerical values for better readability, such as limiting decimal places or handling different data types gracefully.
Below is the **full corrected code** with the `format_value` function defined. I've placed the `format_value` function near the top of the script, right after the imports, to ensure it's available wherever needed in the `run_backtest_ui` function.
```python
# components/run_backtest.py
import streamlit as st
import pandas as pd
import numpy as np
import plotly.express as px
# Importing all necessary functions from utils, metrics, and plots
from utils import (
download_data,
generate_recommendations,
backtest,
fetch_fama_french_factors,
)
from metrics import (
calculate_portfolio_metrics,
calculate_final_score,
calculate_risk_factor_attribution,
get_drawdown_details
)
from plots import (
plot_growth_comparison,
plot_drawdown_comparison,
plot_cagr_over_time,
plot_allocation_pie,
plot_correlation_heatmap,
plot_risk_return_attribution,
plot_rolling_metrics,
plot_correlation_heatmap_comparison,
plot_cagr_comparison,
plot_allocation_comparison,
plot_rolling_metrics_comparison,
plot_risk_return_attribution_comparison,
plot_efficient_frontier_comparison
)
def format_value(x):
"""
Formats the input value to two decimal places if it's a numeric type.
Otherwise, returns the value as is.
"""
if isinstance(x, (int, float, np.number)):
return f"{x:.2f}"
else:
return x
def run_backtest_ui():
# Initialize portfolios in session state if not already initialized
if 'portfolios' not in st.session_state:
st.session_state.portfolios = []
if st.session_state.portfolios:
with st.form("compare_form"):
st.subheader("🔍 Select Portfolios to Compare")
comparison_type = st.selectbox(
"Comparison Type",
["Portfolio vs Benchmark", "Portfolio vs Portfolio"]
)
portfolio_names = [p['name'] for p in st.session_state.portfolios]
if comparison_type == "Portfolio vs Benchmark":
portfolio1 = st.selectbox("Select Portfolio", portfolio_names)
portfolio2 = None
else:
portfolio1 = st.selectbox("Select First Portfolio", portfolio_names, key="p1")
portfolio2 = st.selectbox("Select Second Portfolio", portfolio_names, key="p2")
if portfolio1 == portfolio2:
st.warning("Please select two different portfolios for comparison.")
st.subheader("⚙️ Visualizations Settings")
selected_time_frames = st.multiselect(
"Select Time Frames for CAGR",
options=['Weekly', 'Monthly', 'Quarterly', 'Annually'],
default=['Annually'],
key="selected_time_frames",
help="Choose one or more time frames to view CAGR over different horizons."
)
selected_rolling_periods = st.multiselect(
"Select Rolling Periods (Days)",
options=[30, 90, 180, 252],
default=[252],
key="selected_rolling_periods",
help="Choose one or more periods to view rolling metrics."
)
run = st.form_submit_button("Run Backtest")
if run:
with st.spinner("Running backtest..."):
if comparison_type == "Portfolio vs Benchmark":
# Handle Portfolio vs Benchmark
try:
portfolio = next(p for p in st.session_state.portfolios if p['name'] == portfolio1)
except StopIteration:
st.error("Selected portfolio not found.")
return
price_data = download_data(
portfolio['selected'],
portfolio['start_date'],
portfolio['end_date']
)
if price_data.empty:
st.error("No price data available for the selected portfolio.")
else:
available_selected = [ticker for ticker in portfolio['selected'] if ticker in price_data.columns]
missing_selected = list(set(portfolio['selected']) - set(available_selected))
if missing_selected:
st.warning(f"Excluded tickers with no data: {', '.join(missing_selected)}")
if not available_selected:
st.error(
"No selected tickers have available data for the chosen date range. "
"Please ensure that the ticker symbols are correct and data is available for the specified period."
)
else:
# Extract corresponding allocations
original_allocations = portfolio.get('allocations', [0.0]*len(portfolio.get('selected', [])))
adjusted_allocations = [original_allocations[i] for i, ticker in enumerate(portfolio.get('selected', [])) if ticker in available_selected]
# Error Handling for Allocation-Axis
if len(adjusted_allocations) != len(available_selected):
st.error("Mismatch between allocations and available assets after filtering.")
return
# Normalize allocations to sum to 1
weights = np.array(adjusted_allocations) / 100
total_alloc = weights.sum()
if not np.isclose(total_alloc, 1.0, atol=1e-4):
if total_alloc == 0:
st.error("Total allocations sum to 0. Cannot adjust allocations.")
return
st.warning("Allocations do not sum to 100%. Adjusting allocations proportionally.")
weights = weights / total_alloc
# Final check to ensure weights match the number of assets
if len(weights) != len(available_selected):
st.error("Final allocation weights do not match the number of available assets.")
return
# Calculate per-asset returns
asset_returns = price_data[available_selected].pct_change().dropna()
# Run backtest to get portfolio returns
returns, cum_returns = backtest(
weights,
price_data[available_selected],
portfolio['rebalance_freq'],
portfolio['broker_fee'],
debug=False
)
# Align asset returns with portfolio returns
common_index = returns.index.intersection(asset_returns.index)
asset_returns = asset_returns.loc[common_index]
returns = returns.loc[common_index]
cum_returns = cum_returns.loc[common_index]
if cum_returns.empty:
st.error("Cumulative returns are empty. Check the data and allocations.")
else:
benchmark_symbol = portfolio['benchmark_symbol']
benchmark_data = download_data([benchmark_symbol], portfolio['start_date'], portfolio['end_date'])
if benchmark_symbol not in benchmark_data.columns:
st.error(f"The selected benchmark symbol '{benchmark_symbol}' does not have available data for the chosen period.")
st.stop()
else:
st.success(f"Benchmark '{benchmark_symbol}' data successfully downloaded and will be used for comparison.")
if not benchmark_data.empty:
benchmark_returns = benchmark_data[benchmark_symbol].pct_change().dropna()
benchmark_returns = benchmark_returns.reindex(returns.index, method='ffill').dropna()
common_index_benchmark = returns.index.intersection(benchmark_returns.index)
if common_index_benchmark.empty:
st.error("No overlapping dates between portfolio returns and benchmark returns after alignment.")
st.stop()
returns = returns.loc[common_index_benchmark]
cum_returns = cum_returns.loc[common_index_benchmark]
asset_returns = asset_returns.loc[common_index_benchmark]
benchmark_returns = benchmark_returns.loc[common_index_benchmark]
benchmark_cum_returns = (1 + benchmark_returns).cumprod()
benchmark_metrics = calculate_portfolio_metrics(
benchmark_returns, benchmark_cum_returns, portfolio['rf_rate'], None
)
else:
benchmark_returns = None
benchmark_cum_returns = pd.Series(dtype=float) # Ensure it's defined
benchmark_metrics = {
'Start Balance': "N/A",
'End Balance': "N/A",
'Annualized Return (CAGR)': "N/A",
'Best Year': "N/A",
'Worst Year': "N/A",
'Arithmetic Mean (Monthly)': "N/A",
'Arithmetic Mean (Annualized)': "N/A",
'Geometric Mean (Monthly)': "N/A",
'Geometric Mean (Annualized)': "N/A",
'Standard Deviation (Monthly)': "N/A",
'Standard Deviation (Annualized)': "N/A",
'Downside Deviation (Monthly)': "N/A",
'Maximum Drawdown': "N/A",
'Sharpe Ratio': "N/A",
'Sortino Ratio': "N/A",
'Gain/Loss Ratio': "N/A",
'Skewness': "N/A",
'Excess Kurtosis': "N/A",
'Safe Withdrawal Rate': "N/A",
'Perpetual Withdrawal Rate': "N/A",
'Positive Periods': "N/A",
'Benchmark Correlation': np.nan,
'Beta': np.nan,
'R2': np.nan
}
portfolio_metrics = calculate_portfolio_metrics(
returns, cum_returns, portfolio['rf_rate'], benchmark_returns
)
# Dashboard Header with Key Stats
with st.container():
st.markdown("### 🔑 Key Metrics")
key_metrics = {
'Annualized Return (CAGR)': portfolio_metrics.get('Annualized Return (CAGR)', "N/A"),
'Sharpe Ratio': portfolio_metrics.get('Sharpe Ratio', "N/A"),
'Maximum Drawdown': portfolio_metrics.get('Maximum Drawdown', "N/A")
}
cols = st.columns(len(key_metrics))
for col, (metric, value) in zip(cols, key_metrics.items()):
with col:
st.metric(label=metric, value=value)
# Create Tabs for Organized Sections
tabs = st.tabs(["Overview", "Performance Statistics", "Advanced Metrics", "Drawdowns", "Visualizations"])
with tabs[0]:
st.header("📈 Portfolio Performance Overview")
st.write(f"**Portfolio Name:** {portfolio['name']}")
st.write(f"**Start Date:** {portfolio['start_date'].strftime('%Y-%m-%d')}")
st.write(f"**End Date:** {portfolio['end_date'].strftime('%Y-%m-%d')}")
st.write(f"**Benchmark:** {portfolio['benchmark_symbol']}")
with tabs[1]:
st.header("📊 Performance Statistics")
performance_data_stats = {
'Metric': [
'Start Balance',
'End Balance',
'Annualized Return (CAGR)',
'Standard Deviation (Annualized)',
'Best Year',
'Worst Year',
'Maximum Drawdown',
'Sharpe Ratio',
'Sortino Ratio',
'Benchmark Correlation'
],
'Portfolio': [
portfolio_metrics['Start Balance'],
portfolio_metrics['End Balance'],
f"{portfolio_metrics['Annualized Return (CAGR)']:.2f}%" if not np.isnan(portfolio_metrics['Annualized Return (CAGR)']) else "N/A",
f"{portfolio_metrics['Standard Deviation (Annualized)']:.2f}%" if not np.isnan(portfolio_metrics['Standard Deviation (Annualized)']) else "N/A",
f"{portfolio_metrics['Best Year']:.2f}%" if not np.isnan(portfolio_metrics['Best Year']) else "N/A",
f"{portfolio_metrics['Worst Year']:.2f}%" if not np.isnan(portfolio_metrics['Worst Year']) else "N/A",
f"{portfolio_metrics['Maximum Drawdown']:.2f}%" if not np.isnan(portfolio_metrics['Maximum Drawdown']) else "N/A",
f"{portfolio_metrics['Sharpe Ratio']:.2f}" if not np.isnan(portfolio_metrics['Sharpe Ratio']) else "N/A",
f"{portfolio_metrics['Sortino Ratio']:.2f}" if not np.isnan(portfolio_metrics['Sortino Ratio']) else "N/A",
f"{portfolio_metrics['Benchmark Correlation']:.2f}" if not np.isnan(portfolio_metrics['Benchmark Correlation']) else "N/A"
],
'Benchmark': [
benchmark_metrics.get('Start Balance', "N/A"),
benchmark_metrics.get('End Balance', "N/A"),
f"{benchmark_metrics.get('Annualized Return (CAGR)', np.nan):.2f}%" if not pd.isna(benchmark_metrics.get('Annualized Return (CAGR)', np.nan)) else "N/A",
f"{benchmark_metrics.get('Standard Deviation (Annualized)', np.nan):.2f}%" if not pd.isna(benchmark_metrics.get('Standard Deviation (Annualized)', np.nan)) else "N/A",
f"{benchmark_metrics.get('Best Year', np.nan):.2f}%" if not pd.isna(benchmark_metrics.get('Best Year', np.nan)) else "N/A",
f"{benchmark_metrics.get('Worst Year', np.nan):.2f}%" if not pd.isna(benchmark_metrics.get('Worst Year', np.nan)) else "N/A",
f"{benchmark_metrics.get('Maximum Drawdown', np.nan):.2f}%" if not pd.isna(benchmark_metrics.get('Maximum Drawdown', np.nan)) else "N/A",
f"{benchmark_metrics.get('Sharpe Ratio', np.nan):.2f}" if not pd.isna(benchmark_metrics.get('Sharpe Ratio', np.nan)) else "N/A",
f"{benchmark_metrics.get('Sortino Ratio', np.nan):.2f}" if not pd.isna(benchmark_metrics.get('Sortino Ratio', np.nan)) else "N/A",
f"{benchmark_metrics.get('Benchmark Correlation', np.nan):.2f}" if not pd.isna(benchmark_metrics.get('Benchmark Correlation', np.nan)) else "N/A"
]
}
performance_df_stats = pd.DataFrame(performance_data_stats).set_index('Metric')
st.table(performance_df_stats)
with tabs[2]:
st.header("📋 Detail Comparisons")
with st.container():
st.subheader("🧮 Advanced Metrics")
performance_data_advanced = {
'Metric': list(portfolio_metrics.keys()),
'Portfolio': list(portfolio_metrics.values()),
'Benchmark': list(benchmark_metrics.values())
}
performance_df_advanced = pd.DataFrame(performance_data_advanced).set_index('Metric')
# Replace NaN with empty strings for better visualization
performance_df_advanced = performance_df_advanced.replace(np.nan, "")
# Drop rows where both Portfolio and Benchmark values are NaN
performance_df_advanced_clean = performance_df_advanced.dropna(how='all')
# Format metrics to two decimal places
performance_df_advanced_clean = performance_df_advanced_clean.applymap(format_value)
st.table(performance_df_advanced_clean)
st.markdown("---")
st.write("**Note:** Some metrics in the benchmark column are empty because they are portfolio-specific measurements that can't be calculated for the benchmark alone...")
st.subheader("🔍 Risk Factor Attribution Analysis")
# Define date range based on the overlapping index
start_date = returns.index.min().strftime('%Y-%m-%d')
end_date = returns.index.max().strftime('%Y-%m-%d')
# Fetch actual factor data
factors = fetch_fama_french_factors(start_date, end_date)
# Ensure factors are fetched
if factors.empty:
st.write("Failed to retrieve factor data.")
else:
# Align factors frequency with portfolios
if isinstance(returns.index, pd.DatetimeIndex):
if returns.index.freq is None:
inferred_freq = pd.infer_freq(returns.index)
if inferred_freq is None:
inferred_freq = 'D' # Set default frequency to daily
else:
inferred_freq = returns.index.freq
try:
factors = factors.asfreq(inferred_freq, method='ffill')
except TypeError as e:
st.error(f"Error setting frequency for factors: {e}")
st.stop()
else:
st.error("Returns index is not a DatetimeIndex.")
st.stop()
# Calculate Attribution for Portfolio
attribution, error = calculate_risk_factor_attribution(returns, factors)
if error:
st.error("Risk factor attribution analysis is unavailable for the portfolio.")
else:
if attribution.empty:
st.write("Risk factor attribution analysis is unavailable for the portfolio.")
else:
st.write(f"### Risk Factor Attribution for {portfolio['name']}")
st.table(attribution.fillna(0))
# Visualization
fig_attribution = px.bar(
attribution,
x='Factor',
y='Contribution (%)',
title='Risk Factor Attribution',
template='plotly_dark'
)
st.plotly_chart(fig_attribution, use_container_width=True)
with tabs[3]:
st.header("📉 Detailed Drawdowns")
st.markdown("### 📈 Drawdowns for Portfolio")
portfolio_drawdowns = get_drawdown_details(cum_returns)
if portfolio_drawdowns:
portfolio_drawdowns_df = pd.DataFrame(portfolio_drawdowns)
st.table(portfolio_drawdowns_df)
else:
st.write("No drawdowns detected for the portfolio.")
st.markdown("### 📈 Drawdowns for Benchmark")
benchmark_drawdowns = get_drawdown_details(benchmark_cum_returns)
if benchmark_drawdowns:
benchmark_drawdowns_df = pd.DataFrame(benchmark_drawdowns)
st.table(benchmark_drawdowns_df)
else:
st.write("No drawdowns detected for the benchmark.")
with tabs[4]:
st.header("📊 Visualizations")
if not cum_returns.empty and not benchmark_cum_returns.empty:
plot_growth_comparison(cum_returns, benchmark_cum_returns, labels=["Portfolio", "Benchmark"])
else:
st.warning("Insufficient data to display Growth Comparison. Ensure both portfolio and benchmark have data.")
portfolio_drawdown_series = (cum_returns / cum_returns.expanding().max() - 1) * 100
benchmark_drawdown_series = (benchmark_cum_returns / benchmark_cum_returns.expanding().max() - 1) * 100
plot_drawdown_comparison(portfolio_drawdown_series, benchmark_drawdown_series, labels=["Portfolio", "Benchmark"])
st.subheader("📈 Compound Annual Growth Rate (CAGR) Over Time")
if selected_time_frames:
plot_cagr_over_time(cum_returns, time_frames=selected_time_frames, label="Portfolio")
else:
st.warning("Please select at least one time frame for CAGR.")
# Box Plot for Returns Distribution
st.subheader("📦 Returns Distribution Box Plot")
if isinstance(asset_returns, pd.DataFrame):
# Assuming the index is named 'Date'
if asset_returns.index.name != 'Date':
asset_returns = asset_returns.copy()
asset_returns.index.name = 'Date'
melted_returns = asset_returns.reset_index().melt(id_vars='Date', var_name='Asset', value_name='Return')
fig_box_plot = px.box(
melted_returns,
x='Asset',
y='Return',
title='Returns Distribution Box Plot',
labels={'Return': 'Returns', 'Asset': 'Asset'},
points='all',
hover_data=['Return'],
template='plotly_dark',
color='Asset',
color_discrete_sequence=px.colors.qualitative.Set3
)
st.plotly_chart(fig_box_plot, use_container_width=True)
st.markdown("**Interpretation:** The box plot visualizes the distribution of returns for each asset in the portfolio. The boxes represent the interquartile range (IQR), the line inside the box indicates the median, and the whiskers show the range of the data. Outliers are displayed as individual points.")
elif isinstance(asset_returns, pd.Series):
# Assuming the index is named 'Date'
if asset_returns.index.name != 'Date':
asset_returns = asset_returns.copy()
asset_returns.index.name = 'Date'
melted_returns = asset_returns.reset_index().rename(columns={'Date': 'Date', 0: 'Return'})
fig_box_plot = px.box(
melted_returns,
y='Return',
title='Returns Distribution Box Plot',
labels={'Return': 'Returns'},
points='all',
hover_data=['Return'],
template='plotly_dark'
)
st.plotly_chart(fig_box_plot, use_container_width=True)
st.markdown("**Interpretation:** The box plot visualizes the distribution of returns for the portfolio. The box represents the interquartile range (IQR), the line inside the box indicates the median, and the whiskers show the range of the data. Outliers are displayed as individual points.")
else:
st.warning("Returns data is neither a DataFrame nor a Series.")
# Heatmap of Correlations Between Assets
st.subheader("🔥 Correlation Heatmap of Assets")
plot_correlation_heatmap(asset_returns)
# Add Risk-Return Attribution Analysis
st.subheader("🔍 Risk-Return Attribution Analysis")
plot_risk_return_attribution(returns, weights, label='Portfolio')
# Cumulative Returns Heatmap
st.subheader("🔥 Cumulative Returns Heatmap")
try:
cum_returns_normalized = cum_returns / cum_returns.expanding().max()
fig_cum_heatmap = px.imshow(
cum_returns_normalized.to_frame().T,
labels=dict(x="Date", y="Portfolio", color="Normalized Cumulative Return"),
title="Cumulative Returns Heatmap",
aspect="auto",
color_continuous_scale='Viridis',
template='plotly_dark'
)
st.plotly_chart(fig_cum_heatmap, use_container_width=True)
st.markdown("**Interpretation:** The heatmap visualizes the normalized cumulative returns over time, allowing for an intuitive comparison of portfolio performance across different periods.")
except Exception as e:
st.error(f"Error plotting cumulative returns heatmap: {e}")
# Portfolio Allocation Pie Chart
st.subheader("🥧 Portfolio Allocation Pie Chart")
plot_allocation_pie(weights, available_selected, title="Portfolio Allocation", hover_info="percent+name")
# Rolling Metrics Chart with User-Defined Periods
st.subheader("📉 Rolling Metrics")
if selected_rolling_periods:
plot_rolling_metrics(returns, windows=selected_rolling_periods, label='Portfolio')
else:
st.warning("Please select at least one rolling period to display metrics.")
# Recommendations
st.markdown("### 💡 Recommendations")
recommendations = generate_recommendations(
allocations=[alloc for alloc in portfolio['allocations'] if alloc > 0],
available_selected=[ticker for ticker in available_selected if portfolio['allocations'][available_selected.index(ticker)] > 0]
)
if recommendations:
for rec in recommendations:
st.write(rec)
else:
st.success("Your portfolio allocations are well-balanced!")
# Final Score
st.markdown("### 📝 Final Score")
skipped_metrics_set = set()
portfolio_score = calculate_final_score(portfolio_metrics, benchmark_metrics, global_skipped=skipped_metrics_set)
benchmark_score = calculate_final_score(benchmark_metrics, portfolio_metrics, global_skipped=skipped_metrics_set)
st.write(f"**Portfolio:** {portfolio_score:.2f} / 100")
st.write(f"**Benchmark:** {benchmark_score:.2f} / 100")
# Show skipped metrics once
if skipped_metrics_set:
st.warning(f"Skipped metrics due to invalid values: {', '.join(skipped_metrics_set)}")
# Store results
st.session_state.backtest_results = {
'returns': returns,
'cum_returns': cum_returns,
'weights_a': weights,
'price_data': price_data[available_selected],
'metrics': portfolio_metrics,
'benchmark_metrics': benchmark_metrics,
'performance_df_stats': performance_df_stats, # Corrected variable name
'performance_df_advanced': performance_df_advanced_clean, # Corrected to use the cleaned DataFrame
'benchmark_cum_returns': benchmark_cum_returns
}
elif comparison_type == "Portfolio vs Portfolio" and portfolio1 and portfolio2 and portfolio1 != portfolio2:
# Handle Portfolio vs Portfolio
try:
portfolio_a = next(p for p in st.session_state.portfolios if p['name'] == portfolio1)
portfolio_b = next(p for p in st.session_state.portfolios if p['name'] == portfolio2)
except StopIteration:
st.error("One or both selected portfolios were not found.")
return
# Initialize cum_returns_a and cum_returns_b to prevent UnboundLocalError
cum_returns_a = pd.Series(dtype=float)
cum_returns_b = pd.Series(dtype=float)
# Backtest Portfolio A
price_data_a = download_data(
portfolio_a['selected'],
portfolio_a['start_date'],
portfolio_a['end_date']
)
# Backtest Portfolio B
price_data_b = download_data(
portfolio_b['selected'],
portfolio_b['start_date'],
portfolio_b['end_date']
)
if price_data_a.empty or price_data_b.empty:
st.error("No price data available for one or both selected portfolios.")
else:
# Process Portfolio A
available_a = [ticker for ticker in portfolio_a['selected'] if ticker in price_data_a.columns]
missing_a = list(set(portfolio_a['selected']) - set(available_a))
if missing_a:
st.warning(f"Excluded tickers from {portfolio_a['name']} with no data: {', '.join(missing_a)}")
if not available_a:
st.error(f"No selected tickers have available data for the portfolio '{portfolio_a['name']}'.")
return
# Extract corresponding allocations for Portfolio A
original_allocations_a = portfolio_a.get('allocations', [0.0]*len(portfolio_a.get('selected', [])))
adjusted_allocations_a = [original_allocations_a[i] for i, ticker in enumerate(portfolio_a.get('selected', [])) if ticker in available_a]
if len(adjusted_allocations_a) != len(available_a):
st.error(f"Mismatch between allocations and available assets after filtering for '{portfolio_a['name']}'.")
return
# Normalize allocations to sum to 1
weights_a = np.array(adjusted_allocations_a) / 100
total_alloc_a = weights_a.sum()
if not np.isclose(total_alloc_a, 1.0, atol=1e-4):
if total_alloc_a == 0:
st.error(f"Total allocations for '{portfolio_a['name']}' sum to 0. Cannot adjust allocations.")
return
st.warning(f"Allocations for '{portfolio_a['name']}' do not sum to 100%. Adjusting allocations proportionally.")
weights_a = weights_a / total_alloc_a
# Calculate per-asset returns for Portfolio A
asset_returns_a = price_data_a[available_a].pct_change().dropna()
# Run backtest for Portfolio A
returns_a, cum_returns_a = backtest(
weights_a,
price_data_a[available_a],
portfolio_a['rebalance_freq'],
portfolio_a['broker_fee'],
debug=False
)
# Process Portfolio B
available_b = [ticker for ticker in portfolio_b['selected'] if ticker in price_data_b.columns]
missing_b = list(set(portfolio_b['selected']) - set(available_b))
if missing_b:
st.warning(f"Excluded tickers from {portfolio_b['name']} with no data: {', '.join(missing_b)}")
if not available_b:
st.error(f"No selected tickers have available data for the portfolio '{portfolio_b['name']}'.")
return
# Extract corresponding allocations for Portfolio B
original_allocations_b = portfolio_b.get('allocations', [0.0]*len(portfolio_b.get('selected', [])))
adjusted_allocations_b = [original_allocations_b[i] for i, ticker in enumerate(portfolio_b.get('selected', [])) if ticker in available_b]
if len(adjusted_allocations_b) != len(available_b):
st.error(f"Mismatch between allocations and available assets after filtering for '{portfolio_b['name']}'.")
return
# Normalize allocations to sum to 1
weights_b = np.array(adjusted_allocations_b) / 100
total_alloc_b = weights_b.sum()
if not np.isclose(total_alloc_b, 1.0, atol=1e-4):
if total_alloc_b == 0:
st.error(f"Total allocations for '{portfolio_b['name']}' sum to 0. Cannot adjust allocations.")
return
st.warning(f"Allocations for '{portfolio_b['name']}' do not sum to 100%. Adjusting allocations proportionally.")
weights_b = weights_b / total_alloc_b
# Calculate per-asset returns for Portfolio B
asset_returns_b = price_data_b[available_b].pct_change().dropna()
# Run backtest for Portfolio B
returns_b, cum_returns_b = backtest(
weights_b,
price_data_b[available_b],
portfolio_b['rebalance_freq'],
portfolio_b['broker_fee'],
debug=False
)
# Align the data for comparison
common_dates = returns_a.index.intersection(returns_b.index)
if common_dates.empty:
st.error("No overlapping dates between the two portfolios after alignment.")
return
cum_returns_a = cum_returns_a.loc[common_dates]
cum_returns_b = cum_returns_b.loc[common_dates]
returns_a = returns_a.loc[common_dates]
returns_b = returns_b.loc[common_dates]
asset_returns_a = asset_returns_a.loc[common_dates]
asset_returns_b = asset_returns_b.loc[common_dates]
# Calculate Metrics
metrics_a = calculate_portfolio_metrics(
returns_a, cum_returns_a, portfolio_a['rf_rate'], None
)
metrics_b = calculate_portfolio_metrics(
returns_b, cum_returns_b, portfolio_b['rf_rate'], None
)
# Dashboard Header with Key Stats
with st.container():
st.markdown("### 🔑 Key Metrics")
key_metrics = {
'Annualized Return (CAGR)': ('CAGR', metrics_a.get('Annualized Return (CAGR)', "N/A"), metrics_b.get('Annualized Return (CAGR)', "N/A")),
'Sharpe Ratio': ('Sharpe Ratio', metrics_a.get('Sharpe Ratio', "N/A"), metrics_b.get('Sharpe Ratio', "N/A")),
'Maximum Drawdown': ('Maximum Drawdown', metrics_a.get('Maximum Drawdown', "N/A"), metrics_b.get('Maximum Drawdown', "N/A"))
}
columns = st.columns(len(key_metrics))
for idx, (metric, (label, value_a, value_b)) in enumerate(key_metrics.items()):
with columns[idx]:
st.metric(label=f"{label} ({portfolio_a['name']})", value=value_a)
st.metric(label=f"{label} ({portfolio_b['name']})", value=value_b)
# Create Tabs for Organized Sections
tabs = st.tabs(["Overview", "Performance Statistics", "Advanced Metrics", "Drawdowns", "Visualizations"])
with tabs[0]:
st.header("📈 Portfolios Performance Overview")
# Determine the aligned start and end dates
aligned_start_date = common_dates.min().strftime('%Y-%m-%d')
aligned_end_date = common_dates.max().strftime('%Y-%m-%d')
st.write(f"**Aligned Date Range:** {aligned_start_date} to {aligned_end_date}")
st.write(f"**Portfolio A:** {portfolio_a['name']}")
st.write(f"**Original Start Date:** {portfolio_a['start_date'].strftime('%Y-%m-%d')}")
st.write(f"**Portfolio B:** {portfolio_b['name']}")
st.write(f"**Original Start Date:** {portfolio_b['start_date'].strftime('%Y-%m-%d')}")
with tabs[1]:
st.header("📊 Performance Statistics")
with st.container():
st.subheader("🛠️ Basic Metrics")
performance_data_stats = {
'Metric': [
'Start Balance',
'End Balance',
'Annualized Return (CAGR)',
'Standard Deviation (Annualized)',
'Best Year',
'Worst Year',
'Maximum Drawdown',
'Sharpe Ratio',
'Sortino Ratio',
'Benchmark Correlation'
],
f'{portfolio_a["name"]}': [
metrics_a['Start Balance'],
metrics_a['End Balance'],
metrics_a['Annualized Return (CAGR)'],
metrics_a['Standard Deviation (Annualized)'],
metrics_a['Best Year'],
metrics_a['Worst Year'],
metrics_a['Maximum Drawdown'],
metrics_a['Sharpe Ratio'],
metrics_a['Sortino Ratio'],
metrics_a['Benchmark Correlation']
],
f'{portfolio_b["name"]}': [
metrics_b['Start Balance'],
metrics_b['End Balance'],
metrics_b['Annualized Return (CAGR)'],
metrics_b['Standard Deviation (Annualized)'],
metrics_b['Best Year'],
metrics_b['Worst Year'],
metrics_b['Maximum Drawdown'],
metrics_b['Sharpe Ratio'],
metrics_b['Sortino Ratio'],
metrics_b['Benchmark Correlation']
]
}
performance_df_stats = pd.DataFrame(performance_data_stats).set_index('Metric')
st.table(performance_df_stats)
with tabs[2]:
st.header("📋 Detail Comparisons")
with st.container():
st.subheader("🧮 Advanced Metrics")
performance_data_advanced = {
'Metric': list(metrics_a.keys()),
f'{portfolio_a["name"]}': list(metrics_a.values()),
f'{portfolio_b["name"]}': list(metrics_b.values())
}
performance_df_advanced = pd.DataFrame(performance_data_advanced).set_index('Metric')
# Replace NaN with empty strings for better visualization
performance_df_advanced = performance_df_advanced.replace(np.nan, "")
# Drop rows where both Portfolio A and Portfolio B values are NaN
performance_df_advanced_clean = performance_df_advanced.dropna(how='all', subset=[f'{portfolio_a["name"]}', f'{portfolio_b["name"]}'])
st.table(performance_df_advanced_clean)
st.markdown("---")
st.write("**Note:** Some metrics may be specific to certain portfolios or may not be directly comparable. Ensure to interpret the metrics in the context of each portfolio's strategy and objectives.")
st.subheader("🔍 Risk Factor Attribution Analysis")
# Define date range based on the overlapping index
start_date = common_dates.min().strftime('%Y-%m-%d')
end_date = common_dates.max().strftime('%Y-%m-%d')
# Fetch actual factor data
factors = fetch_fama_french_factors(start_date, end_date)
# Ensure factors are fetched
if factors.empty:
st.write("Failed to retrieve factor data.")
else:
# Align factors frequency with portfolios
if isinstance(returns_a.index, pd.DatetimeIndex):
if returns_a.index.freq is None:
inferred_freq = pd.infer_freq(returns_a.index)
if inferred_freq is None:
inferred_freq = 'D' # Set default frequency to daily
else:
inferred_freq = returns_a.index.freq
try:
factors = factors.asfreq(inferred_freq, method='ffill')
except TypeError as e:
st.error(f"Error setting frequency for factors: {e}")
st.stop()
else:
st.error("Returns index is not a DatetimeIndex.")
st.stop()
# Calculate Attribution for Portfolio A
attribution_a, error_a = calculate_risk_factor_attribution(returns_a, factors)
# Calculate Attribution for Portfolio B
attribution_b, error_b = calculate_risk_factor_attribution(returns_b, factors)
if error_a or error_b:
st.error("Risk factor attribution analysis is unavailable for one or both portfolios.")
else:
if attribution_a.empty or attribution_b.empty:
st.write("Risk factor attribution analysis is unavailable for one or both portfolios.")
else:
st.write(f"**Risk Factor Attribution for {portfolio_a['name']}:**")
st.table(attribution_a)
fig_attribution_a = px.bar(
attribution_a,
x='Factor',
y='Contribution (%)',
title=f'Risk Factor Attribution for {portfolio_a["name"]}',
labels={'Contribution (%)': 'Contribution (%)'},
template='plotly_dark'
)
st.plotly_chart(fig_attribution_a, use_container_width=True)
st.write(f"**Risk Factor Attribution for {portfolio_b['name']}:**")
st.table(attribution_b)
fig_attribution_b = px.bar(
attribution_b,
x='Factor',
y='Contribution (%)',
title=f'Risk Factor Attribution for {portfolio_b["name"]}',
labels={'Contribution (%)': 'Contribution (%)'},
template='plotly_dark'
)
st.plotly_chart(fig_attribution_b, use_container_width=True)
# Combined Bar Chart for Comparison
combined_attribution = pd.concat([
attribution_a.set_index('Factor')['Contribution (%)'],
attribution_b.set_index('Factor')['Contribution (%)']
], axis=1)
combined_attribution.columns = [portfolio_a['name'], portfolio_b['name']]
combined_attribution = combined_attribution.fillna(0)
st.markdown(f"### 📊 Risk Factor Attribution Comparison")
fig_combined_attribution = px.bar(
combined_attribution,
x=combined_attribution.index,
y=combined_attribution.columns,
title='Risk Factor Attribution Comparison',
labels={'value': 'Contribution (%)', 'Factor': 'Risk Factor'},
template='plotly_dark',
barmode='group'
)
st.plotly_chart(fig_combined_attribution, use_container_width=True)
with tabs[3]:
st.header("📉 Detailed Drawdowns")
st.markdown("### 📈 Drawdowns for Portfolio A")
portfolio_drawdowns = get_drawdown_details(cum_returns_a)
if portfolio_drawdowns:
portfolio_drawdowns_df = pd.DataFrame(portfolio_drawdowns)
st.table(portfolio_drawdowns_df)
else:
st.write("No drawdowns detected for Portfolio A.")
st.markdown("### 📈 Drawdowns for Portfolio B")
portfolio_b_drawdowns = get_drawdown_details(cum_returns_b)
if portfolio_b_drawdowns:
portfolio_b_drawdowns_df = pd.DataFrame(portfolio_b_drawdowns)
st.table(portfolio_b_drawdowns_df)
else:
st.write("No drawdowns detected for Portfolio B.")
with tabs[4]:
st.header("📊 Visualizations")
if not cum_returns_a.empty and not cum_returns_b.empty:
plot_growth_comparison(cum_returns_a, cum_returns_b, labels=[portfolio_a['name'], portfolio_b['name']])
else:
st.warning("Insufficient data to display Growth Comparison. Ensure both portfolios have data.")
portfolio_a_drawdown_series = (cum_returns_a / cum_returns_a.expanding().max() - 1) * 100
portfolio_b_drawdown_series = (cum_returns_b / cum_returns_b.expanding().max() - 1) * 100
plot_drawdown_comparison(portfolio_a_drawdown_series, portfolio_b_drawdown_series, labels=[portfolio_a['name'], portfolio_b['name']])
st.subheader("📈 Compound Annual Growth Rate (CAGR) Over Time")
if selected_time_frames:
plot_cagr_comparison(cum_returns_a, cum_returns_b, time_frames=selected_time_frames)
else:
st.warning("Please select at least one time frame for CAGR.")
# Box Plot for Returns Distribution
st.subheader("📦 Returns Distribution Box Plot")
# For portfolio A
if isinstance(asset_returns_a, pd.DataFrame):
# Ensure the index is named 'Date'
if asset_returns_a.index.name != 'Date':
asset_returns_a = asset_returns_a.copy()
asset_returns_a.index.name = 'Date'
melted_returns_a = asset_returns_a.reset_index().melt(id_vars='Date', var_name='Asset', value_name='Return')
fig_box_plot_a = px.box(
melted_returns_a,
x='Asset',
y='Return',
title='Returns Distribution Box Plot - Portfolio A',
labels={'Return': 'Returns', 'Asset': 'Asset'},
points='all',
hover_data=['Return'],
template='plotly_dark',
color='Asset',
color_discrete_sequence=px.colors.qualitative.Set3
)
st.plotly_chart(fig_box_plot_a, use_container_width=True)
st.markdown("**Interpretation:** The box plot visualizes the distribution of returns for each asset in Portfolio A. The boxes represent the interquartile range (IQR), the line inside the box indicates the median, and the whiskers show the range of the data. Outliers are displayed as individual points.")
elif isinstance(asset_returns_a, pd.Series):
# Ensure the index is named 'Date'
if asset_returns_a.index.name != 'Date':
asset_returns_a = asset_returns_a.copy()
asset_returns_a.index.name = 'Date'
melted_returns_a = asset_returns_a.reset_index().rename(columns={'Date': 'Date', 'Return': 'Return'})
fig_box_plot_a = px.box(
melted_returns_a,
y='Return',
title='Returns Distribution Box Plot - Portfolio A',
labels={'Return': 'Returns'},
points='all',
hover_data=['Return'],
template='plotly_dark'
)
st.plotly_chart(fig_box_plot_a, use_container_width=True)
st.markdown("**Interpretation:** The box plot visualizes the distribution of returns for Portfolio A. The box represents the interquartile range (IQR), the line inside the box indicates the median, and the whiskers show the range of the data. Outliers are displayed as individual points.")
else:
st.warning("Returns data for Portfolio A is neither a DataFrame nor a Series.")
# For portfolio B
if isinstance(asset_returns_b, pd.DataFrame):
# Ensure the index is named 'Date'
if asset_returns_b.index.name != 'Date':
asset_returns_b = asset_returns_b.copy()
asset_returns_b.index.name = 'Date'
melted_returns_b = asset_returns_b.reset_index().melt(id_vars='Date', var_name='Asset', value_name='Return')
fig_box_plot_b = px.box(
melted_returns_b,
x='Asset',
y='Return',
title='Returns Distribution Box Plot - Portfolio B',
labels={'Return': 'Returns', 'Asset': 'Asset'},
points='all',
hover_data=['Return'],
template='plotly_dark',
color='Asset',
color_discrete_sequence=px.colors.qualitative.Set3
)
st.plotly_chart(fig_box_plot_b, use_container_width=True)
st.markdown("**Interpretation:** The box plot visualizes the distribution of returns for each asset in Portfolio B. The boxes represent the interquartile range (IQR), the line inside the box indicates the median, and the whiskers show the range of the data. Outliers are displayed as individual points.")
elif isinstance(asset_returns_b, pd.Series):
# Ensure the index is named 'Date'
if asset_returns_b.index.name != 'Date':
asset_returns_b = asset_returns_b.copy()
asset_returns_b.index.name = 'Date'
melted_returns_b = asset_returns_b.reset_index().rename(columns={'Date': 'Date', 'Return': 'Return'})
fig_box_plot_b = px.box(
melted_returns_b,
y='Return',
title='Returns Distribution Box Plot - Portfolio B',
labels={'Return': 'Returns'},
points='all',
hover_data=['Return'],
template='plotly_dark'
)
st.plotly_chart(fig_box_plot_b, use_container_width=True)
st.markdown("**Interpretation:** The box plot visualizes the distribution of returns for Portfolio B. The box represents the interquartile range (IQR), the line inside the box indicates the median, and the whiskers show the range of the data. Outliers are displayed as individual points.")
else:
st.warning("Returns data for Portfolio B is neither a DataFrame nor a Series.")
# Correlation Heatmap Comparison
st.subheader("🔥 Correlation Heatmap Comparison")
plot_correlation_heatmap_comparison(asset_returns_a, asset_returns_b)
# Efficient Frontier Comparison
st.subheader("📈 Efficient Frontier Comparison")
plot_efficient_frontier_comparison(asset_returns_a, asset_returns_b, num_portfolios=1000, rf=portfolio_a['rf_rate'])
# Risk-Return Attribution Comparison
st.subheader("🔍 Risk-Return Attribution Comparison")
plot_risk_return_attribution_comparison(
returns_a, weights_a,
returns_b, weights_b,
label_a=portfolio_a['name'],
label_b=portfolio_b['name']
)
# Recommendations
st.markdown("### 💡 Recommendations")
recommendations_a = generate_recommendations(
allocations=[alloc for alloc in portfolio_a['allocations'] if alloc > 0],
available_selected=[ticker for ticker in available_a if portfolio_a['allocations'][available_a.index(ticker)] > 0]
)
recommendations_b = generate_recommendations(
allocations=[alloc for alloc in portfolio_b['allocations'] if alloc > 0],
available_selected=[ticker for ticker in available_b if portfolio_b['allocations'][available_b.index(ticker)] > 0]
)
if recommendations_a:
st.write(f"**{portfolio_a['name']} Recommendations:**")
for rec in recommendations_a:
st.write(rec)
else:
st.success(f"{portfolio_a['name']} allocations are well-balanced!")
if recommendations_b:
st.write(f"**{portfolio_b['name']} Recommendations:**")
for rec in recommendations_b:
st.write(rec)
else:
st.success(f"{portfolio_b['name']} allocations are well-balanced!")
# Final Score
st.markdown("### 📝 Final Score")
portfolio_score_a = calculate_final_score(metrics_a, metrics_b)
portfolio_score_b = calculate_final_score(metrics_b, metrics_a)
st.write(f"**{portfolio_a['name']}:** {portfolio_score_a:.2f} / 100")
st.write(f"**{portfolio_b['name']}:** {portfolio_score_b:.2f} / 100")
# Store results
st.session_state.backtest_results = {
'returns_a': returns_a,
'cum_returns_a': cum_returns_a,
'weights_a': weights_a,
'price_data_a': price_data_a[available_a],
'metrics_a': metrics_a,
'returns_b': returns_b,
'cum_returns_b': cum_returns_b,
'weights_b': weights_b,
'price_data_b': price_data_b[available_b],
'metrics_b': metrics_b,
'performance_df_stats_a': performance_df_stats.loc[:, portfolio_a['name']],
'performance_df_advanced_a': performance_df_advanced_clean[[f'{portfolio_a["name"]}']],
'performance_df_stats_b': performance_df_stats.loc[:, portfolio_b['name']],
'performance_df_advanced_b': performance_df_advanced_clean[[f'{portfolio_b["name"]}']]
}
st.success("Backtest completed successfully!")
else:
st.warning("Please add at least one portfolio to run backtest.")
```
### Explanation of the Fix:
1. **Definition of `format_value`:**
- **Purpose:** The `format_value` function is designed to format numerical values to two decimal places for better readability. If the value is not a numeric type, it returns the value as-is.
- **Implementation:** The function checks if the input `x` is an instance of `int`, `float`, or any NumPy numeric type (`np.number`). If it is, it formats the number to two decimal places using an f-string. Otherwise, it simply returns the input value without modification.
2. **Placement of `format_value`:**
- The `format_value` function is placed right after the import statements for better visibility and to ensure it's available throughout the `run_backtest_ui` function.
3. **Usage of `format_value`:**
- The function is used within the `tabs[2]` section, specifically when formatting the `performance_df_advanced_clean` DataFrame. This ensures that all numerical metrics in the advanced performance table are consistently formatted, enhancing the readability of the output.
4. **Additional Corrections:**
- **Variable Name Correction:** In the `st.session_state.backtest_results` dictionary, the variable `performance_df_stats_formatted` was referenced, but it wasn’t defined anywhere. This has been corrected to `performance_df_stats`, ensuring consistency and avoiding potential future errors.
- **Consistent Formatting:** By defining and using the `format_value` function, all relevant numerical metrics are now uniformly formatted across the application, providing a cleaner and more professional appearance.
### Recommendations:
- **Error Handling:** While the `format_value` function adds clarity to your data presentation, ensure that all other parts of your code also have adequate error handling to manage unexpected inputs or data inconsistencies.
- **Modular Code:** Consider modularizing repetitive tasks or similar formatting needs by defining more helper functions if necessary. This can further enhance the maintainability and scalability of your codebase.
- **Documentation:** Adding docstrings or comments to your helper functions (like `format_value`) can be beneficial for future reference and for other developers who might work on the code.
By incorporating the `format_value` function and making the above adjustments, your backtest UI should function correctly without encountering the `NameError`. Always ensure that all functions and variables used in your code are properly defined and imported to avoid such issues.0223e0fa62099c484f51e7d0
Analyze the following message and determine if it's suitable for an AI to respond to.
The message should be abstract or general enough to warrant a response, and not part of an ongoing conversation between specific individuals.
Rules:
1. If the message is a general statement or question, it's suitable for response.
2. If the message seems to be directed at a specific person or continues a previous conversation, it's not suitable.
3. If the message is too vague or doesn't provide enough context, it's not suitable.
Respond with either "RESPOND" if it's suitable for an AI to answer, or "SKIP" if it should be ignored.
Message: work hard and get rewards
RESPOND
d6e0ea186303b0c65d0478b0
Analyze the following message and determine if it's suitable for an AI to respond to.
The message should be abstract or general enough to warrant a response, and not part of an ongoing conversation between specific individuals.
Rules:
1. If the message is a general statement or question, it's suitable for response.
2. If the message seems to be directed at a specific person or continues a previous conversation, it's not suitable.
3. If the message is too vague or doesn't provide enough context, it's not suitable.
Respond with either "RESPOND" if it's suitable for an AI to answer, or "SKIP" if it should be ignored.
Message: xion will deliver succesfully
SKIP
83f725bd50711aa9e277fad5hi, how are you?
Hello! I'm just a computer program, so I don't have feelings, but I'm here to help you. How can I assist you today?
4a597c044d839522fe4d8244hi, how are you?
Hi! I'm just a program, so I don't have feelings, but I'm here and ready to help you. How can I assist you today?
ffb3dda0820ce077255df16b
Analyze the following message and determine if it's suitable for an AI to respond to.
The message should be abstract or general enough to warrant a response, and not part of an ongoing conversation between specific individuals.
Rules:
1. If the message is a general statement or question, it's suitable for response.
2. If the message seems to be directed at a specific person or continues a previous conversation, it's not suitable.
3. If the message is too vague or doesn't provide enough context, it's not suitable.
Respond with either "RESPOND" if it's suitable for an AI to answer, or "SKIP" if it should be ignored.
Message: be active on XION community
SKIP
27c9d403ba0b878ebe2f3edfhi, how are you?
Hello! I'm just a computer program, so I don't have feelings, but I'm here and ready to help you. How can I assist you today?