DEMO for INSPIRATION: investment portfolio chart dashboard

Built with Streamlit :sheaf_of_rice:

Hey Streamlit community!

I wanted to share something I’ve been working on and give a huge thanks to the Streamlit team for open-sourcing this amazing platform.

– OpenFieldbook – is a data visualization app.

When I first started coding–around 2010–Streamlit didn’t exist (and if it did, it certainly didn’t exist in the same environment it does today). To get an app live on-the-web with basic functionality required thousands of development hours and a hefty technical cost. At a minimum. Design required thousands more hours.

Back then, I was involved with a company that didn’t financially survive the tech development upstart. I don’t know what the statistics are, but I’d imagine that more businesses fail for this reason than you’d expect.

No longer is that the case. Streamlit, as well as other open-source packages and technological advancements through time, paved the way for the eventual “1-person, billion dollar startup.” A future I look forward to; and I know it’s in you with a bit of persistence:

  • Rapid prototyping: From idea to working prototype in hours
  • Python-native: No need to context-switch between languages
  • Beautiful by default: Professional-looking apps without wrestling with CSS
  • Community: Amazing ecosystem and helpful community like this one

Homepage: https://openfieldbook.com

At times I’ve struggled with the limitations of using a third-party platform; but, I think it has led to ingenuity and, importantly, necessary simplicity. I’d love to discuss all of the outside-the-box tips and tricks I’ve learned along the way, but I feel my time is running short for this post.

If you demo the site and find a technique you’d like to understand more about, please send me the question about it.

I currently host on AWS using five or six services; costing under $200 per mo. (I know this may sound like a lot for some–and I probably purchased more EC2 compute than was necessary for development purposes–but as I’ve struggled through this process, I see the potential for that cost to scale efficiently with thoughtful Streamlit development with data apps; cache_data and cache_resource and fragment are used liberally throughout my code and are great for saving on data transfer and database pings.)

My app consists of approximately 25,000 lines of code for reference; exclusive of all third-party modules (e.g. Streamlit, pandas, etc).

Demo

OpenFieldbook started as an investment portfolio dashboard–still is–and now I’m in the last phase of integrating core A/I capabilities.

Here, you can access a live, no-registration demo. This demo doesn’t really display all Streamlit is capable of, but it’s what I have available outside of a login screen.

The whole website was built with Streamlit as the front-end interpreter and server. I encourage you to give the A/I deep dive a try on the homepage (note: this is business / investment focused output).

Streamlit will notice that I keep removing their logo implementation–which I don’t like, but I understand the intention to brand their work. In exchange for removing it, I’ve included a link back to their site on the footer of my OpenFieldbook inside of the login. (Streamlit Team, please don’t make it any more difficult to remove in the future!)

Thank You, Streamlit Team! :folded_hands:

I’m incredibly grateful that Streamlit is open-source under the Apache 2.0 license. The fact that you’ve made such a powerful tool freely available to developers worldwide is genuinely inspiring. The ease of development, the thoughtful API design, and the vibrant ecosystem you’ve fostered have made building OpenFieldbook a joy rather than a chore.

Giving Back: Lessons Learned & Code Insights :light_bulb:

I’d love to help you out if I can–it’s owed as reciprocity at the very least. Over the next week or so, I’ll monitor this post for any questions you may have; preferably about specific “how-to’s”.

Fuel for thought: from my perspective the initial possibilities for A/I are great, but they’re not everything. The ‘A/I wrapper’ is just as, if not more, important than the LLM’s raw output.

That is, what can you do with the A/I that others aren’t or can’t? How is your app unique relative to the (practically unlimited) free queries offered by all of the big name A/I services? I suggest much value is added in the curation and maintenance of data and content–precisely what Streamlit was built for.

I hope OpenFieldbook can inspire your next level of development.

Since you’re reading this, we probably share common interests–AND–I can’t do this alone forever! Perhaps there’s an opportunity for us to develop together. Please reach out if this is you! Or if you know someone.

And if none of that applies but the app provides you value, I’m happy to have you as a subscriber instead.


Would love to hear your thoughts and feedback!

Thanks again to the entire Streamlit team and community! :rocket:

2 Likes

If you can share/opensource a scaled down version of UI with sample data it would be helpful for all.

The site looks great. UI is super.

Hi Ryan!

Great work, I really liked what you developed!

I am working in finance as a risk manager and I am also a Python/Streamlit enthusiast.

I am interested on developing finance/investment related apps. A question regarding the portfolio upload of the users, is this done manually, by uploading csv file of holdings etc.?

Have you delved deeper on this side by providing functionalities of API connectivity with other platforms? Retail brokers for example?

Happy to discuss further details.

Feel free to share your email/linkedin and I can reach out.

In the meantime, keep up the amazing work and I wish you all the best luck in your endeavors.

Best regards,

Andreas Baros.

I would be happy to share much of the code. There are a couple immediate barriers to doing so:

1/ I’d have to go through it and remove any sensitive areas and
2/ the main problem is this code won’t run by plugging in secrets is because there are multiple independent AWS services integrated throughout; which would need to be modified for this to be meaningful as a github repo.

This will be a project unto itself.

If the goal is to distribute the knowledge, I think the best and quickest way is to find a specific piece of the app that looks interesting and isolate it, maybe present it as a silo’d demo and share independently.

In all cases, happy to work with someone. Here’s a good email to reach me. All the best, and thanks for the kind words.

1 Like

Thanks Andreas -

A question regarding the portfolio upload of the users, is this done manually, by uploading csv file of holdings etc.?

Yes, it’s possible.

Long answer: It’s also possible to change the portfolio on the user interface. The portfolio weights are then stored in two areas: in a field in the user’s dynamodb database, and as a .pkl file on an S3 datalake. The redundancy is because: as the user logs in, the user’s last viewed portfolio is picked up in one acquisition with login credentials, saving a trip to the datalake. The datalake is there for storage and backup of multiple user portfolios.

Have you delved deeper on this side by providing functionalities of API connectivity with other platforms? Retail brokers for example?

Not at this time, I haven’t. I’m not opposed to it. Happy to work with someone on it, feel free to reach out. The major concern would be accurately processing / calculating the transaction input values.

Here’s a good email to reach me. Thanks for your kind words. I’m happy to continue the conversation.

Looks amazing, great job! I am building financial tools for my personal investing and really like the styling you have for the portfolio_tools. It looks like you are embedding some other dashboarding tool or framework outside of native Streamlit is that correct? Would love to know how you achieved those reports if you don’t mind sharing.

Attached is a screenshot of the page I’m referring to. Thanks!

1 Like

Hi @cgage1 - Thanks. I am using Plotly for the charts. I started out with Altair, which I’ve always been partial to, but plotly had more customization options. I think much of the appeal is design work (e.g. standardizing colors) as opposed to original implementation.

Here is the function for this sunburst chart.

import plotly.graph_objects as go

def create_sunburst_visualization(
    labels, parents, values, performances, 
    color_scale, color_min, color_max
    ):
    fig = go.Figure()
    
    # Add sunburst trace - preserve input ordering
    fig.add_trace(go.Sunburst(
        labels=labels,
        parents=parents,
        values=values,
        customdata=performances,
        marker=dict(
            colors=performances,
            colorscale=color_scale,
            cmin=color_min,
            cmax=color_max,
            showscale=False,
            colorbar=dict(
                title="Performance (%)",
            )
        ),
        sort=False,  # Preserve input ordering
        textfont=dict(
            family="Sans-serif",
            size=18,
        ),
        hovertemplate="<span style='font-size: 18px;'>%{label}<br><br><b>Amt: %{value:$,.0f}</b></span><br><br><span style='font-size: 16px;'>Perf: %{customdata:+0.2%}</span></br><extra></extra>",
        
    ))
    
    # Update layout
    cht_title = "portfolio pie" if st.session_state.get("performance_weight", "Absolute") == "Absolute" else "contribution heatmap"
    fig.update_layout(
        title=dict(
            text="" if st.session_state.get('remove_chart_titles', False) else cht_title,
            x=0.95,
            y=1,
            xanchor='right',
            yanchor='top',
            font=dict(
                family="Sans-serif",  # Font family (e.g. "Arial", "Helvetica", "Times New Roman")
                size=24,              # Font size in pixels
                color=st.session_state.user.get('second_color',"#b16ee6"),     # Font color (hex, rgb, rgba)
                weight=100,          # Font weight (100-900, normal=400, bold=700)
                style="normal",      # Font style ("normal", "italic")
                textcase="lower"
            )
        ),
        margin=dict(l=0, r=0, t=35, b=10),
        height=478,
        width=min((len([_ for _ in labels if _ not in parents]) + 1) * 75, 1200),
    )

    return fig

There are 3 tabs that contain the set of charts (Snapshot, Historical, Risk); each tab containing 3 or 4 charts driven by the segmented_control up top and the pills-timeframe selector below.

# Segmented control
if st.session_state.snap_chart_toggle_key in [0, None]:
    
    # Create and display sunburst chart
    _fig = CPNT.create_sunburst_visualization(
        labels, parents, values, performances,
        color_scale, data_min, data_max
        )
    st.plotly_chart(_fig, width='content')

Each tab is one encompassing function and st.fragment’d to save on compute during re-runs. The pill-control that drives the time frame changes the data calculations was the biggest challenge, imo-- aligning the data across all of the charts.

In this case, all the data is driven by two primary dataframes: one with a timeseries of stock prices, and another for portfolio holdings. The resulting visuals is about manipulating and transforming those two data sets.

Feel free to get more specific.

1 Like

Hello good work,

how did you get this rendering on these buttons ?

image

each button displays a page

image

1 Like

If you’re referring to the coloring, it was with a lot of trial and error on custom CSS. Toward the top of each page, I inject an HTML style tag. Some CSS classes and ids are generated dynamically with each streamlit update, and that requires some retesting. I’m running streamlit v1.49 and these were the tags I identified. Toggles are separate identifiers, for example.

Streamlit recently released v1.50 with custom theme coloring options, which probably negates much of what I did.

top of page styling

st.markdown(view_frames.styling_func())

in view_frames.py

import streamlit as st
import socket

def styling_func():
    hostname = socket.gethostname()
    is_server = True if "desktop" not in hostname.lower() else False

    segmented_class = "st-emotion-cache-1r4qj8v" if is_server else "st-emotion-cache-1r4qj8v" 
    segmented_class2 = "st-emotion-cache-um0spe" if is_server else "st-emotion-cache-um0spe"
    segmented_class3 = "st-emotion-cache-17sg2lo" if is_server else "st-emotion-cache-17sg2lo" 

    pill_class = "st-emotion-cache-64326f" if is_server else "st-emotion-cache-64326f" 
    pill_class2 = "st-emotion-cache-ne3lcw" if is_server else "st-emotion-cache-1mf87dp" 

    return f"""
<style>


.{segmented_class}:hover {{
    border: 1px solid {secondary_color};
}}
.{segmented_class}:hover .{segmented_class2} {{
    color: {secondary_color};
}}
.{pill_class},
.{pill_class}:hover,
.{pill_class}:active,
.{pill_class}:focus,
.{segmented_class3}, /* local machine segmented control */
.{segmented_class3}:hover,
.{segmented_class3}:active,
.{segmented_class3}:focus {{
    border: 1px solid {secondary_color};
    color: {secondary_color};
    background: {UI.hex_to_rgba(secondary_color, 0.1)};
}}
/* Target the pills text color */
button[kind="pills"] > .{pill_class2} {{ 
    color: {secondary_color};
}}
.{pill_class} > .{pill_class2} {{ 
    color: {secondary_color};
}}
.{pill_class}:hover {{ 
    border: 1px solid {secondary_color};
}}
</style>"""

Segmented control

snap_toggle_options = {
    0: ":material/explosion:", # starburst
    1: ":material/leaderboard:", # vbars
    2: ":material/more_vert:", # strip plot
    3: ":material/grid_view:", # grid
}

def return_chart_toggle(title, option_map, key_mod, first_default_int=0):
    st.segmented_control(title, options=option_map.keys(),
        format_func=lambda option: option_map[option], 
        default=list(option_map.keys())[first_default_int],
        selection_mode="single", label_visibility='collapsed',
        key=f"{key_mod}chart_toggle_key", 
    )

return_chart_toggle(
    "Chart type", UDC.snap_toggle_options, key_mod="snap_", 
    first_default_int=st.session_state.get('snapshot_default', None)
    )

Pills

snap_time_options = {
    0: ["1d", 1],
    1: ["5d", 5],
    2: ["3m", 63],
    3: ["ytd", 0],
    4: ["1y", 252],
}

def return_timeframe_toggle(title, option_map, key_mod, index_default=4):
    st.pills(title, options=option_map.keys(),
        format_func=lambda option: option_map[option][0],
        selection_mode="single", label_visibility='collapsed',
        key=f"{key_mod}timeframes_key", default=index_default
    )

return_timeframe_toggle("Snapshot time pills", 
     UDC.snap_time_options, key_mod="snap_", index_default=2
    )

general structure of calling segmented control to display different charts:

with st_col1:
    
    return_chart_toggle(
        "Chart type", UDC.snap_toggle_options, key_mod="snap_", 
        first_default_int=st.session_state.get('snapshot_default', None)
        )

    if st.session_state.snap_chart_toggle_key in [0, None]:
        # Create and display sunburst chart
        st.plotly_chart(_fig, width='content')

    if st.session_state.snap_chart_toggle_key == 1:
        # Create and display other chart
        st.plotly_chart(_fig2, width='content')
1 Like

No, I’m not talking about the coloring, but about the icons.

Ah, ok. I updated my response above. Additionally, each tab is it’s own fragmented function, so upon re-run of the app, the charts update to the latest segmented_control and pills selections.

3 Likes