Spaces:
Runtime error
Runtime error
import streamlit as st | |
import plotly.express as px | |
from datetime import date, timedelta | |
from data_creator import create_market_cap_dict, gen_rebased_df, ids2names_dict, names2ids_dict, create_assets, gen_symbols, create_histories_df, create_unix_dates, create_returns_df, create_rebased_df, date_range | |
from plot_creator import write_coins, write_coins_custom, create_chart_df, load_images, gen_performance_df, add_drawdown | |
from port_creator import gen_all_returns, markowitz_weights_dict, uniform_weights_dict, ids_with_histories, uniform, create_port_rtns, markowitz_weights, create_weights_df | |
from risk_metrics import max_drawdown | |
st.markdown( | |
""" | |
<style> | |
.css-1xsoh1l { | |
font-size: 0px; | |
} | |
.css-1xsoh1l{ | |
color: rgb(120 190 33); | |
} | |
.css-jhf39w { | |
color: rgba(120, 190, 33, 1); | |
} | |
.css-jv3mmh { | |
background-color: rgb(120, 190, 33); | |
} | |
</style> | |
""", | |
unsafe_allow_html = True | |
) | |
# load start and end dates for investment analysis | |
lookback_years = 5 # max date range for backtest will be: lookback_years - 1 | |
start_date = date.today() - timedelta(365) | |
end_date = date.today() | |
if 'start_date' not in st.session_state: | |
st.session_state.start_date = start_date | |
st.session_state.end_date = end_date | |
if 'max_coins' not in st.session_state: | |
st.session_state.max_coins = 8 | |
if 'start_id' not in st.session_state: | |
st.session_state.start_id = 1 # this is the id of the selected portfolio. | |
# Pull down histories from coincap, and create dataframes for historic prices, | |
# returns and rebased cumulative price; histories_df, returns_df, and | |
# rebased_df, respectively. | |
assets_json = create_assets(total_coins=50) | |
symbols, names, coin_ids = gen_symbols(assets_json) | |
ids2symbols = ids2names_dict(coin_ids, symbols) | |
ids2names_dict=ids2names_dict(coin_ids, names) | |
names2ids_dict = names2ids_dict(names, coin_ids) | |
market_cap_dict = create_market_cap_dict(assets_json) | |
start_unix, end_unix = create_unix_dates(today=date.today(), lookback_years=lookback_years) | |
histories_df = create_histories_df(coin_ids, start_unix, end_unix) | |
# Create list of coin ids with full hisoties over the backtest period | |
ids_with_histories = ids_with_histories(histories_df, | |
st.session_state.start_date, st.session_state.end_date) | |
names_with_histories = list(map(ids2names_dict.get, ids_with_histories)) | |
def change_date_range(): | |
st.session_state.start_date = st.session_state.myslider[0] | |
st.session_state.end_date = st.session_state.myslider[1] | |
st.write(''' | |
N.B. This app is in Beta, it will be buggy and some of the calculations may be | |
erroneous. **It is deeeeeefinitely not investment advice**. | |
''', unsafe_allow_html = True) | |
with st.expander("Explain this app π€"): | |
st.subheader("What's this all about then, eh?") | |
st.write(''' | |
This app allows you to graph the historic performance of a portfolio of | |
your choosing against an individual coin of your choosing. You can | |
compare the historic performance of all available assets (portfolios and coins) | |
in the overview table at the bottom of the page. | |
The 'Portfolio vs coin' chart displays the historic performance of the selected | |
portfolio and coin for the selected date range, rebased to $1 at the start | |
date. | |
To switch focus between the selected portfolio and coin, use the radio buttons | |
above the graph. For the asset under focus the "maximum drawdown" is drawn | |
on the graph (this is the maximum loss the asset suffered over the selected | |
period). A high level view of the performance of the asset under focus is also | |
given inside the 'Asset performance' expander. | |
The 'Overview of performance section' sets out performance metrics for all of | |
the portfolios and coins over the selected investment period. The table can | |
be sorted based on the metric of interest. | |
There are two pre-defined portfolio construction strategies, but you can also | |
define your own allocation using the 'Create your own' option in the 'Select | |
portfolio strategy' dropdown box in the sidebar. | |
Pre-defined portfolios: | |
- Uniform - An equal propotion of your initial investment is allocated to each coin in the 'investment set' (i.e. the total number of coins available for investment). | |
- Markowitz - Your initial investment is allocated to each coin to achieve the portfolio with the highest sharpe ratio in the 365 day period prior to the investment start date. For some date ranges and investment sets, the optimiser used in the background of this app cannot find an optimal solution in which case the Markowitz weights will not be updated and you will see a warning at the top of the screen. I have used the default settings in the PyPortfolioOpt python package to construct this portfolio. | |
To select how many coins you would like in your investment set use the | |
'Maximum number of coins in portfolio' inside the 'Portfolio construction | |
settings' expander. | |
To adjust the date range for the portfolio backtest using the slider | |
widget. | |
To create your own portfolio: | |
1. Select 'Create your own' in the 'Select portfolio strategy' dropdown; | |
2. Select the maximum number of coins in your portfolio; | |
3. Select the relative weights for each of these assets; | |
4. Choose a name for your portfolio and click add portfolio; | |
5. Click the 'Update viewer' button. | |
The data for this app has been generated using the coincap api and is updated | |
every day to include the previous day's closing prices. | |
''') | |
# Add select slider to allow | |
date_list = date_range(end_date,lookback_years-1) | |
start_port_date, end_port_date = st.select_slider( | |
'Select date range (max 4 years)', | |
key="myslider", | |
options=date_list, | |
#value=(date.today() - timedelta(365), date.today()), | |
value = (st.session_state.start_date, st.session_state.end_date), | |
on_change=change_date_range | |
) | |
with st.sidebar: | |
st.subheader("Select a coin and a portfolio to compare on the graph") | |
with st.expander("Portfolio construction settings", expanded = False): | |
st.session_state.max_coins = st.number_input( | |
"Maximum number of coins in portfolio", | |
min_value=1, | |
max_value=50, | |
value=8, | |
key='max_coin_selector', | |
help=''' | |
Coins will be added to your "investment set" in order of largest market cap. | |
The "investment set" is the group of assets from which your portfolio is | |
constructed. Depending on the portfolio strategy you choose, not all of the | |
assets in your investment set will be included in your portfolio. | |
''' | |
) | |
# calculate weghts for the uniform and markowitz pfs | |
uniform_weights = uniform_weights_dict(ids_with_histories[:int(st.session_state.max_coins)]) | |
update_Markowitz = True | |
try: | |
markowitz_weights = markowitz_weights_dict(histories_df, | |
st.session_state.start_date ,ids_with_histories[:int(st.session_state.max_coins)], analysis_days=365) | |
except: | |
update_Markowitz = False | |
st.warning('Markowitz weights could not be updated for this date range') | |
if 'strategy_dict' not in st.session_state: | |
st.session_state.strategy_dict = {'Uniform': uniform_weights, 'Markowitz':markowitz_weights} | |
else: | |
st.session_state.strategy_dict['Uniform'] = uniform_weights | |
if update_Markowitz == True: | |
st.session_state.strategy_dict['Markowitz'] = markowitz_weights | |
#if "strategy_dict" not in st.session_state: | |
# st.session_state.strategy_dict=strategy_dict | |
with st.sidebar: | |
selected_coin = st.selectbox( | |
'Select coin', | |
names_with_histories | |
) | |
portfolio_type = st.selectbox( | |
'Select portfolio strategy', | |
['Create your own'] + (list(st.session_state.strategy_dict.keys())), | |
index = st.session_state.start_id | |
) | |
# calculate returns for the portfolios and add to it the rebased df for assets | |
# with hisories. This is the new returns_df | |
rebased_df = gen_rebased_df(histories_df, ids_with_histories, | |
st.session_state.start_date, st.session_state.end_date) | |
all_returns_df = gen_all_returns(rebased_df, ids_with_histories, st.session_state.strategy_dict) | |
if portfolio_type != 'Create your own': | |
st.session_state.portfolio_type = portfolio_type | |
st.subheader("Portfolio vs coin") | |
focus=st.radio("Focus on",("Portfolio π","Coin π")) | |
chart_df = create_chart_df(all_returns_df, st.session_state.portfolio_type, names2ids_dict[selected_coin]) | |
fig = px.line(chart_df, x=chart_df.index, y='Value (USD)', color='Asset') | |
if focus == "Portfolio π": | |
fig, port_dd, port_dd_start, port_dd_end = add_drawdown(fig, all_returns_df, st.session_state.portfolio_type) | |
else: | |
fig, coin_dd, coin_dd_start, coin_dd_end = add_drawdown(fig, all_returns_df, names2ids_dict[selected_coin]) | |
st.write(fig) | |
with st.expander("Asset performance"): | |
if focus == "Portfolio π": | |
st.subheader("{} portfolio performance".format(st.session_state.portfolio_type)) | |
cols = st.columns([1,3]) | |
outlay = cols[0].number_input('Initial $ amount', min_value=0, value=1000, | |
step=1) | |
final_amount = outlay*all_returns_df[st.session_state.portfolio_type][-1] | |
max_loss=outlay*port_dd | |
with cols[1]: | |
st.markdown('''For an initial investment of **${:,}**'''.format(int(outlay)), unsafe_allow_html = True) | |
st.markdown('''You would have ended up with **${:,}**'''.format(int(final_amount)), unsafe_allow_html = True) | |
st.markdown('''You would have suffered a maximum loss of **{:.0f}%** of your portfolio value | |
between **{}** and **{}**'''.format(port_dd*100, port_dd_start, port_dd_end), unsafe_allow_html = True) | |
else: | |
st.subheader("{} coin performance".format(selected_coin)) | |
cols = st.columns([1,3]) | |
outlay = cols[0].number_input('Initial $ amount', min_value=0, value=1000, | |
step=1) | |
final_amount = outlay*all_returns_df[names2ids_dict[selected_coin]][-1] | |
max_loss=outlay*coin_dd | |
with cols[1]: | |
st.markdown('''For an initial investment of **${:,}**'''.format(int(outlay)), unsafe_allow_html = True) | |
st.markdown('''You would have ended up with **${:,}**'''.format(int(final_amount)), unsafe_allow_html = True) | |
st.markdown('''You would have suffered a maximum loss of **{:.0f}%** of your investment value | |
between **{}** and **{}**'''.format(coin_dd*100, coin_dd_start, coin_dd_end), unsafe_allow_html = True) | |
non_zero_coins = [key for key in st.session_state.strategy_dict[st.session_state.portfolio_type].keys() if st.session_state.strategy_dict[st.session_state.portfolio_type][key]>0] | |
with st.sidebar: | |
if portfolio_type == 'Create your own': | |
st.markdown("Bespoke portfolio weights (relative):" , unsafe_allow_html=False) | |
bespoke_weights = write_coins_custom(names_with_histories[:int(st.session_state.max_coins)]) | |
#bespoke_weights = write_bespoke_coins(names_with_histories[:st.session_state.max_coins]) | |
bespoke_cols = st.columns(2) | |
bespoke_cols[0].write(" ") | |
bespoke_cols[0].write(" ") | |
add_bespoke = bespoke_cols[0].button("Add portfolio", key='bespoke_button') | |
bespoke_name = bespoke_cols[1].text_input("Choose portfolio name") | |
if add_bespoke: | |
if bespoke_name=="" or bespoke_name in all_returns_df.columns: | |
st.warning("Please give your portfolio a unique name") | |
else: | |
beskpoke_weights_dict={} | |
for i, wt in enumerate(bespoke_weights): | |
beskpoke_weights_dict[coin_ids[i]] = wt | |
st.session_state.strategy_dict[bespoke_name] = beskpoke_weights_dict | |
st.session_state.start_id = len(st.session_state.strategy_dict) | |
#st.session_state.selected_assets.append(bespoke_name) | |
st.success("Porfolio added, update viewer to see results") | |
st.button('Update viewer', on_click = change_date_range) | |
#st.button('Update viewer', on_click = change_date_range) | |
else: | |
st.markdown(st.session_state.portfolio_type + " portfolio weights (%):" , unsafe_allow_html=False) | |
write_coins(non_zero_coins, st.session_state.strategy_dict[st.session_state.portfolio_type], ids2names_dict) | |
performance_df = gen_performance_df(all_returns_df, market_cap_dict, st.session_state.strategy_dict) | |
st.subheader("Overview of performance of all available assets") | |
st.dataframe(performance_df.style.background_gradient(cmap='Greens', | |
subset=['Risk / return', 'Total return %']) | |
.background_gradient(cmap='Reds', | |
subset=['Annual vol', 'Max loss %']).format("{:,.2f}", | |
subset=['Risk / return', 'Total return %', 'Annual vol', | |
'Max loss %']).format("{:,.0f}", subset=['Market cap $M'])) | |