<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Build Log on Lenny Kiruthu</title>
        <link>https://lennykiruthu.xyz/multi-asset-portfolio-analytics-platform/building/</link>
        <description>Recent content in Build Log on Lenny Kiruthu</description>
        <generator>Hugo -- gohugo.io</generator>
        <language>en-us</language>
        <lastBuildDate>Sat, 18 Apr 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://lennykiruthu.xyz/multi-asset-portfolio-analytics-platform/building/index.xml" rel="self" type="application/rss+xml" /><item>
            <title>Shipping Auth: Why I Built It Myself with FastAPI</title>
            <link>https://lennykiruthu.xyz/multi-asset-portfolio-analytics-platform/building/2026-04-18-auth/</link>
            <pubDate>Sat, 18 Apr 2026 00:00:00 +0000</pubDate>
            <guid>https://lennykiruthu.xyz/multi-asset-portfolio-analytics-platform/building/2026-04-18-auth/</guid>
            <description>&lt;h1 id=&#34;shipping-auth-why-i-built-it-myself-with-fastapi&#34;&gt;Shipping Auth: Why I Built It Myself with FastAPI&#xA;&lt;/h1&gt;&lt;p&gt;The platform now has a working authentication system. Users can register, log in, and get a personal dashboard tied to their account. Here&amp;rsquo;s what I built and why.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 id=&#34;what-i-shipped&#34;&gt;What I shipped&#xA;&lt;/h2&gt;&lt;p&gt;A FastAPI service that handles:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;code&gt;POST /auth/register&lt;/code&gt; — creates a user record with a hashed password&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;POST /auth/login&lt;/code&gt; — validates credentials, returns a signed JWT&lt;/li&gt;&#xA;&lt;li&gt;Token validation middleware used by all protected endpoints&lt;/li&gt;&#xA;&lt;li&gt;A session state pattern in Streamlit that stores the JWT and passes it on every API call&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;The Streamlit app calls the FastAPI service on login. If the token is valid, you see the app. If not, you see the login screen. Simple.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 id=&#34;why-not-just-use-supabase-auth&#34;&gt;Why not just use Supabase Auth?&#xA;&lt;/h2&gt;&lt;p&gt;I considered it. Supabase Auth is genuinely good — it handles email verification, OAuth providers, session management, and it has a Postgres backend you can query directly.&lt;/p&gt;&#xA;&lt;p&gt;The reason I didn&amp;rsquo;t use it: I want to own the full stack. Every external service I depend on is a constraint on how I can deploy, how I can price, and how I can eventually move. Since I&amp;rsquo;m planning to eventually self-host everything on a VM in a specific jurisdiction (partly for latency reasons when I start pulling African market data), I didn&amp;rsquo;t want authentication to be a cloud dependency I couldn&amp;rsquo;t replicate.&lt;/p&gt;&#xA;&lt;p&gt;Rolling my own also meant I understood every part of it. There are no hidden session tables, no magic token refresh flows I don&amp;rsquo;t control.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 id=&#34;what-it-cost-me&#34;&gt;What it cost me&#xA;&lt;/h2&gt;&lt;p&gt;About four days I didn&amp;rsquo;t expect to spend. JWT libraries in Python are fine but the Streamlit session state pattern required some care — Streamlit reruns the entire script on every interaction, so you have to be deliberate about where the token lives and when to check it.&lt;/p&gt;&#xA;&lt;p&gt;I also had to handle the case where Streamlit and FastAPI are running as separate Docker containers. The API calls from Streamlit needed to hit the FastAPI service by its Docker Compose service name (&lt;code&gt;http://api:8000&lt;/code&gt;), not &lt;code&gt;localhost&lt;/code&gt;. That caught me for longer than I&amp;rsquo;d like to admit.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 id=&#34;whats-next&#34;&gt;What&amp;rsquo;s next&#xA;&lt;/h2&gt;&lt;p&gt;The auth system is stable. Next up: finishing the Portfolio Analytics tab in Streamlit. The transaction ledger works, the dbt models are computing portfolio performance, I just need to wire up the charts.&lt;/p&gt;&#xA;</description>
        </item><item>
            <title>The dbt Layer: How I Model a Portfolio from Transactions Up</title>
            <link>https://lennykiruthu.xyz/multi-asset-portfolio-analytics-platform/building/2026-04-10-dbt/</link>
            <pubDate>Fri, 10 Apr 2026 00:00:00 +0000</pubDate>
            <guid>https://lennykiruthu.xyz/multi-asset-portfolio-analytics-platform/building/2026-04-10-dbt/</guid>
            <description>&lt;h1 id=&#34;the-dbt-layer-how-i-model-a-portfolio-from-transactions-up&#34;&gt;The dbt Layer: How I Model a Portfolio from Transactions Up&#xA;&lt;/h1&gt;&lt;p&gt;The hardest part of building a portfolio analytics system isn&amp;rsquo;t getting prices. It&amp;rsquo;s accurately reconstructing what you actually held on any given day, and computing returns from that.&lt;/p&gt;&#xA;&lt;p&gt;Here&amp;rsquo;s how I do it in dbt.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 id=&#34;the-core-problem&#34;&gt;The core problem&#xA;&lt;/h2&gt;&lt;p&gt;A transaction ledger tells you &lt;em&gt;events&lt;/em&gt;: &amp;ldquo;on this date, I bought X shares of Y at price Z.&amp;rdquo; To compute portfolio performance, you need &lt;em&gt;states&lt;/em&gt;: &amp;ldquo;on every day from then until now, I held N shares of Y.&amp;rdquo;&lt;/p&gt;&#xA;&lt;p&gt;Reconstructing that state from events is the job of &lt;code&gt;int_daily_holdings&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 id=&#34;the-staging-layer&#34;&gt;The staging layer&#xA;&lt;/h2&gt;&lt;p&gt;Three staging models clean and type the raw sources:&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;&lt;code&gt;stg_raw_prices&lt;/code&gt;&lt;/strong&gt; — Cleans the yFinance data. Handles null close prices (which appear on market holidays in the raw feed), casts dates properly, and renames columns to a consistent schema.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;&lt;code&gt;stg_fred_macro&lt;/code&gt;&lt;/strong&gt; — Cleans FRED indicator data. The FRED API returns data in a long format (series_id, date, value) which I keep as-is here and pivot later.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;&lt;code&gt;stg_transactions&lt;/code&gt;&lt;/strong&gt; — Cleans user transaction records. Normalises tickers to uppercase, validates that quantity and price are positive, and joins in the user&amp;rsquo;s ID.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 id=&#34;int_daily_holdings--the-core-model&#34;&gt;&lt;code&gt;int_daily_holdings&lt;/code&gt; — the core model&#xA;&lt;/h2&gt;&lt;p&gt;This model generates one row per (user, ticker, date) for every day between a user&amp;rsquo;s first transaction and today.&lt;/p&gt;&#xA;&lt;p&gt;The logic:&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;For each (user, ticker), find all BUY and SELL events sorted by date.&lt;/li&gt;&#xA;&lt;li&gt;Generate a spine of all trading days using the prices table.&lt;/li&gt;&#xA;&lt;li&gt;For each day, compute the cumulative net position: sum of BUY quantities minus sum of SELL quantities for all transactions on or before that date.&lt;/li&gt;&#xA;&lt;li&gt;Filter out rows where net position is zero (before any buy, or after a full sell).&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;The spine-based approach is important. I use &lt;code&gt;stg_raw_prices&lt;/code&gt; to generate the date spine rather than a calendar table, because I only need market days and the prices table already filters to those.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 id=&#34;fct_daily_returns&#34;&gt;&lt;code&gt;fct_daily_returns&lt;/code&gt;&#xA;&lt;/h2&gt;&lt;p&gt;Joins &lt;code&gt;int_daily_holdings&lt;/code&gt; against &lt;code&gt;stg_raw_prices&lt;/code&gt; to produce:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Market value of each holding on each day (quantity × close price)&lt;/li&gt;&#xA;&lt;li&gt;Daily return (today&amp;rsquo;s value / yesterday&amp;rsquo;s value - 1)&lt;/li&gt;&#xA;&lt;li&gt;Cumulative return from first purchase&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;This is the main fact table that powers the portfolio time series charts.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 id=&#34;the-gold-layer&#34;&gt;The gold layer&#xA;&lt;/h2&gt;&lt;p&gt;Four mart models sit on top:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;&lt;code&gt;gold_p_timeseries&lt;/code&gt;&lt;/strong&gt; — Aggregates &lt;code&gt;fct_daily_returns&lt;/code&gt; to portfolio level (summed across all tickers per user per day). The time series that drives the main chart.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;&lt;code&gt;gold__performance&lt;/code&gt;&lt;/strong&gt; — Scalar KPIs per user: total return, annualised return, max drawdown, Sharpe ratio (approximated).&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;&lt;code&gt;gold_ticker_kpis&lt;/code&gt;&lt;/strong&gt; — Per-ticker breakdown: holding period, cost basis, current value, unrealised P&amp;amp;L.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;&lt;code&gt;gold_macro_context&lt;/code&gt;&lt;/strong&gt; — Joins &lt;code&gt;fct_macro_regimes&lt;/code&gt; to the portfolio time series so the frontend can shade the chart by macro regime.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 id=&#34;what-id-do-differently&#34;&gt;What I&amp;rsquo;d do differently&#xA;&lt;/h2&gt;&lt;p&gt;The date spine approach works but it means &lt;code&gt;int_daily_holdings&lt;/code&gt; is a large model — one row per user per ticker per trading day. For two users with 10 tickers over 5 years, that&amp;rsquo;s already ~25,000 rows. It scales fine in Postgres for personal use, but if I ever want to support hundreds of users I&amp;rsquo;ll need to think about incremental materialisation more carefully.&lt;/p&gt;&#xA;&lt;p&gt;dbt&amp;rsquo;s incremental models would help here. I&amp;rsquo;ve kept everything as &lt;code&gt;table&lt;/code&gt; for now because it keeps the logic simple and the data volumes are small. I&amp;rsquo;ll revisit this when it becomes a problem.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 id=&#34;macro-regime-classification&#34;&gt;Macro regime classification&#xA;&lt;/h2&gt;&lt;p&gt;The &lt;code&gt;fct_macro_regimes&lt;/code&gt; model deserves its own post. The short version: I use a set of FRED indicators (yield curve slope, unemployment trend, industrial production) to classify each month into one of four regime buckets. These get joined onto the portfolio time series so you can see how your portfolio performed in different macro environments. It&amp;rsquo;s rough but surprisingly useful context.&lt;/p&gt;&#xA;</description>
        </item></channel>
</rss>
