<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Steffan Kharmaaiarvi</title>
    <description>The latest articles on DEV Community by Steffan Kharmaaiarvi (@steffankharmaaiarvi).</description>
    <link>https://dev.to/steffankharmaaiarvi</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4103031%2F877a28bf-1cb6-4bda-b1fe-9a7d2da8026f.jpg</url>
      <title>DEV Community: Steffan Kharmaaiarvi</title>
      <link>https://dev.to/steffankharmaaiarvi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/steffankharmaaiarvi"/>
    <language>en</language>
    <item>
      <title>My broker said I'd made $18,000. The number was an aggregate over a query nobody could show me.</title>
      <dc:creator>Steffan Kharmaaiarvi</dc:creator>
      <pubDate>Mon, 31 Aug 2026 16:00:00 +0000</pubDate>
      <link>https://dev.to/steffankharmaaiarvi/my-broker-said-id-made-18000-the-number-was-an-aggregate-over-a-query-nobody-could-show-me-cgi</link>
      <guid>https://dev.to/steffankharmaaiarvi/my-broker-said-id-made-18000-the-number-was-an-aggregate-over-a-query-nobody-could-show-me-cgi</guid>
      <description>&lt;p&gt;The number sat on my broker's home screen, green, precise to two decimals. Roughly eighteen thousand dollars, earned from investing, over the whole period I'd been at it.&lt;/p&gt;

&lt;p&gt;I looked at it for a long time and realised I didn't know what it included.&lt;/p&gt;

&lt;p&gt;Dividends — reinvested ones, or only the ones that landed as cash? Before or after the tax withheld in a country I don't live in? Closed positions, or only open ones? Fees? Measured from my first-ever deposit, or from the day I opened &lt;em&gt;this&lt;/em&gt; account and transferred everything across?&lt;/p&gt;

&lt;p&gt;Each of those moves the number. Some move it by thousands.&lt;/p&gt;

&lt;p&gt;That's a familiar shape if you write software: &lt;strong&gt;an aggregate whose query you can't see.&lt;/strong&gt; I spent a while looking for the drill-down and eventually understood there wasn't one to find. The breakdown didn't exist. Nobody was keeping the events — not the broker, not my spreadsheet, not any of the tools I tried.&lt;/p&gt;

&lt;p&gt;I'm a self-taught engineer, 24, and I've been contributing to a portfolio monthly for years with financial independence as the actual goal. The strategy is unfashionably boring — broad market exposure, automatic monthly transfer, reinvest, don't touch it — and it worked. The portfolio reached six figures through contributions and time, not through a trade that went right.&lt;/p&gt;

&lt;p&gt;Then my income changed, my contributions got uneven, and the balance stopped functioning as feedback. It still went up. But "the balance went up" now had at least four meanings that demanded different responses from me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I contributed a lot, the market was flat.&lt;/li&gt;
&lt;li&gt;I contributed almost nothing, the market carried it.&lt;/li&gt;
&lt;li&gt;I contributed steadily and both worked.&lt;/li&gt;
&lt;li&gt;I contributed nothing, the market fell, and dividends plus a favourable exchange rate papered over it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One number, four stories, no way to tell them apart. So I started reconstructing the events by hand, and found five bugs — not in any one product, but in the data model almost all of them share.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 1: the balance is a materialized view with no source table
&lt;/h2&gt;

&lt;p&gt;Every tool I used stored the same thing: a set of positions with prices. Current holdings, current value, correct to the cent.&lt;/p&gt;

&lt;p&gt;That model throws away most of what happened. My portfolio didn't stay the same shape — I spent a few years pursuing dividend income toward a $500/month target, then came round to the view that during accumulation, income you immediately reinvest is mostly a taxable event with extra steps. What I converged on was simpler: a global accumulating fund as the core, short-term treasuries, gold.&lt;/p&gt;

&lt;p&gt;Look at my holdings today and you'd conclude I'd always been a three-fund accumulator. That's false, and it means the actual history — including the parts I got wrong — isn't learnable from the data.&lt;/p&gt;

&lt;p&gt;A holdings screen is a snapshot of the conclusion. I wanted the working:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;positions  ← what every tracker stores
   ↑
   │  fold(events)
   │
events     ← what nobody stores
  deposit, buy, sell, dividend, fee,
  withholding, fx_conversion, residence_change
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Positions are derivable from events. Events are &lt;em&gt;not&lt;/em&gt; derivable from positions. Once you write it that way the direction of the arrow settles the design argument. Contributions in particular stop being a bookkeeping entry that makes the pie bigger and become a first-class record: dated, distinguished from reinvested income, with rhythm, streaks and gaps visible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 2: &lt;code&gt;return&lt;/code&gt; is not one function, and two correct implementations disagree on the sign
&lt;/h2&gt;

&lt;p&gt;This is the one that turned a preference into a real problem. I computed my return one way, then another way, and got a different answer — not slightly different, opposite sign.&lt;/p&gt;

&lt;p&gt;Minimal reproduction:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;th&gt;Portfolio value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1 Jan&lt;/td&gt;
&lt;td&gt;Deposit $1,000&lt;/td&gt;
&lt;td&gt;$1,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1 Jul&lt;/td&gt;
&lt;td&gt;Holdings up 50%&lt;/td&gt;
&lt;td&gt;$1,500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1 Jul&lt;/td&gt;
&lt;td&gt;Deposit $9,000 — the plan is working&lt;/td&gt;
&lt;td&gt;$10,500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;31 Dec&lt;/td&gt;
&lt;td&gt;Holdings fall 10%&lt;/td&gt;
&lt;td&gt;$9,450&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Time-weighted return&lt;/strong&gt; — what funds and indices publish — deliberately cancels out the effect of deposit timing by chaining sub-period returns between cash flows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;twr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sub_period_returns&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Geometric chain. Deposit timing has no effect by construction.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;growth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;sub_period_returns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;growth&lt;/span&gt; &lt;span class="o"&gt;*=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;growth&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

&lt;span class="nf"&gt;twr&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mf"&gt;0.50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;0.10&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;   &lt;span class="c1"&gt;# 0.35  → +35%
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Money-weighted return&lt;/strong&gt; — XIRR — dates every cash flow and solves for the rate that discounts them to zero:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;xnpv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;flows&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;          &lt;span class="c1"&gt;# flows: [(date, amount)], deposits negative
&lt;/span&gt;    &lt;span class="n"&gt;t0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;flows&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;t0&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;365.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;flows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;xirr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;flows&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;                &lt;span class="c1"&gt;# bisection: no derivative, no divergence
&lt;/span&gt;    &lt;span class="n"&gt;lo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;0.9999&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;10.0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;mid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lo&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;hi&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;xnpv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;flows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;lo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mid&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                    &lt;span class="n"&gt;hi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mid&lt;/span&gt;
    &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lo&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;hi&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;

&lt;span class="nf"&gt;xirr&lt;/span&gt;&lt;span class="p"&gt;([(&lt;/span&gt;&lt;span class="n"&gt;jan1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1_000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jul1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;9_000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dec31&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;9_450&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;
&lt;span class="c1"&gt;# -0.0978 with ACT/365 to 31 Dec; -0.0973 measured over a full year → −9.7%
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both are correct. The strategy had a great year; the investor lost $550, because 90% of the money only showed up for the bad half.&lt;/p&gt;

&lt;p&gt;A tracker that renders one of these and labels it "your return" is answering a question you may not have asked. I don't think that's dishonesty — it's compression. One percentage is a nicer product decision than two percentages and an explanation. But the compression is exactly where the information I wanted was going.&lt;/p&gt;

&lt;p&gt;Once I saw it the whole vocabulary came apart. There are at least four distinct things called "return":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Unrealized gain&lt;/strong&gt; — revaluation of what I still hold, against what I paid. Says nothing about anything sold, nothing about income.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Realized gain&lt;/strong&gt; — what I locked in by closing positions. Real, spendable, usually taxable somewhere.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Personal return (XIRR)&lt;/strong&gt; — the annualised rate my actual money earned, each deposit weighted by how long it was invested. The only common metric that admits a January deposit and a December deposit are not equal contributors to the year.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Portfolio return (TWR)&lt;/strong&gt; — how the strategy performed with flows removed. The only one comparable to an index, because it's the only one not contaminated by how much money happened to be in at the time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For an uneven contributor, the gap between the last two isn't noise, it's the finding. A wide divergence means the timing of my contributions, not my fund choices, dominated the year. That's actionable in a way neither number is alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 3: rates applied at read time instead of event time
&lt;/h2&gt;

&lt;p&gt;Classic temporal-data mistake, and it's everywhere in this domain.&lt;/p&gt;

&lt;p&gt;If a tool converts a contribution I made three years ago at &lt;em&gt;today's&lt;/em&gt; exchange rate, my savings history silently resizes itself every time the currency moves. I open the app in March and I contributed $47,000 lifetime; I open it in June and I contributed $44,100. I did not withdraw anything.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# wrong: history is recomputed against the present
&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;fx_rate_now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;flows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# right: a flow is stamped at the rate on its own date; only
# what you hold today is valued at today's rate
&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fx_rate_at_trade_date&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;flows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same rule as storing a price with an order line instead of joining to the product table at render time. Sounds pedantic until you've watched your own record of what you contributed change while you weren't looking, and had to decide which version to believe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 4: tax is a function of three variables, and one of them is time
&lt;/h2&gt;

&lt;p&gt;Here's the part I got most wrong at the start.&lt;/p&gt;

&lt;p&gt;I assumed a fund is a fund: pick the index, find the cheapest vehicle tracking it, buy it. Ticker and expense ratio, done. Inside a single country that's correct advice. Across borders it isn't, because a foreign dividend can pass through two independent tax layers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step one, at source.&lt;/strong&gt; The fund's country takes its cut before the money leaves. For a non-US investor in a US-domiciled fund the statutory US rate is 30%; a treaty commonly reduces that to 15%, but only where your broker holds valid documentation for you, and that paperwork expires.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step two, at home.&lt;/strong&gt; Your country of residence then taxes the same dividend under its own rules.&lt;/p&gt;

&lt;p&gt;The steps are independent, which produces the result that surprises everybody: moving somewhere with no dividend tax does not make step one disappear.&lt;/p&gt;

&lt;p&gt;Step one alone, on an illustrative portfolio throwing off $571/year in dividends from US-domiciled funds:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tax residence&lt;/th&gt;
&lt;th&gt;US treaty status&lt;/th&gt;
&lt;th&gt;Withheld at source&lt;/th&gt;
&lt;th&gt;Reaches you&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;United Arab Emirates&lt;/td&gt;
&lt;td&gt;No US tax treaty&lt;/td&gt;
&lt;td&gt;30% · $171&lt;/td&gt;
&lt;td&gt;$400&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Portugal&lt;/td&gt;
&lt;td&gt;Treaty rate on portfolio dividends&lt;/td&gt;
&lt;td&gt;15% · $86&lt;/td&gt;
&lt;td&gt;$485&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Germany&lt;/td&gt;
&lt;td&gt;Treaty rate on portfolio dividends&lt;/td&gt;
&lt;td&gt;15% · $86&lt;/td&gt;
&lt;td&gt;$485&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Same funds, same amounts, same dates. Different residence. Eighty-five dollars a year before anyone has picked a single stock, recurring annually.&lt;/p&gt;

&lt;p&gt;So the withholding rate isn't a column on the instrument. It's &lt;code&gt;rate(fund_domicile, tax_residence, as_of_date)&lt;/code&gt; — and &lt;code&gt;as_of_date&lt;/code&gt; matters because people move. When residence changes the portfolio doesn't move; not one share changes hands. But the rate on every &lt;em&gt;future&lt;/em&gt; dividend changes, and sometimes what you're permitted to buy changes too. The honest answer to "how much tax did this portfolio lose?" is the rate that applied at the time, changing partway through, applied to the dividends actually paid in each period.&lt;/p&gt;

&lt;p&gt;There's an elegant wrinkle: an Irish-domiciled UCITS fund holding US stocks pays 15% at the fund level under the US–Ireland treaty, and Ireland charges nothing further on distributions to non-residents. So an investor in a country with no US treaty can end up at 15% instead of 30% on the same underlying index, purely because of where the fund is registered.&lt;/p&gt;

&lt;p&gt;The obvious conclusion is that everyone outside the US should hold Irish UCITS. I believed that for about a week, then ran it per holding, and it fell apart:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;switch A:  net  +$32/yr   worth doing
switch B:  net   −$7/yr   the fund barely pays dividends, so there was
                          almost no withholding to save — and the UCITS
                          alternative's higher expense ratio is charged
                          against the entire position, not just the income
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Counting only tax saved makes every row look free. The tax saving scales with &lt;strong&gt;dividend yield&lt;/strong&gt;; the expense-ratio penalty scales with &lt;strong&gt;your entire balance&lt;/strong&gt;. Different denominators. Which wins depends on the fund, your portfolio size, your residence, and the fund's own costs — and it can flip as the position grows. Lower withholding doesn't mean better; it means one term in the equation improved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 5: the state transition nobody emits an event for
&lt;/h2&gt;

&lt;p&gt;A non-US person's estate can be exposed to US estate tax on US-situs assets above $60,000 — a category that includes US-domiciled ETFs and individual US stocks — at rates reaching 40% on the excess. Not income tax. Estate tax. Whether it bites, and how hard, depends on treaty coverage, available deductions, how assets are held, and the circumstances of the estate. It's a line worth knowing you've crossed, not a bill anyone can compute for you.&lt;/p&gt;

&lt;p&gt;A long-term index investor crosses it in an ordinary month of an ordinary year. No email, no warning, no change of state anywhere — because from the dashboard's point of view nothing happened. The threshold isn't in the model, so no event fires.&lt;/p&gt;

&lt;p&gt;I'd crossed it long before I knew it existed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule I ended up caring about most
&lt;/h2&gt;

&lt;p&gt;When a metric can't be computed, say so. Never print a zero.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# a hole in the history and a genuine zero must not render identically
&lt;/span&gt;&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Metric&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Decimal&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;confidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Literal&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exact&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;approximate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unavailable&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;      &lt;span class="c1"&gt;# "no valuation recorded around 2024-03-11 cash flow"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;TWR needs a portfolio valuation around every external cash flow. Where one is missing, the standard workaround is an approximation — and it should be &lt;em&gt;labelled&lt;/em&gt; as an approximation, not blended into a headline figure. You cannot reconstruct a valuation nobody recorded. I tried.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I built
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://borderfolio.app" rel="noopener noreferrer"&gt;BorderFolio&lt;/a&gt; — a contribution-first portfolio tracker for long-term international investors. Statements in (PDF, CSV, a screenshot of a broker app, or typed by hand), history out. No broker credentials and no permanent connection, because I didn't want to hold anyone's login, including my own.&lt;/p&gt;

&lt;p&gt;It keeps contributions as a first-class record, splits the result into its actual components (contributions, market growth, income, fees and withholding, realized and unrealized), reports personal return and portfolio return side by side, keeps monthly snapshots so the past stays the past, and resolves withholding per instrument from fund domicile and configured residence — showing both sides of a structural switch, not just the flattering one.&lt;/p&gt;

&lt;p&gt;What it can't do, out loud: it's statement-import based rather than live API, so you upload something once a month. Every calculation is only as good as the records imported, and a missing statement is a hole the tool shows rather than interpolates over. The portfolio-return number genuinely improves as monthly history accumulates, and I can't make that instant. The cross-border figures are informational estimates from published treaty rates and fund documents — not tax advice, and blind to your personal circumstances. Nothing in it, including my own portfolio's shape, is a recommendation.&lt;/p&gt;

&lt;p&gt;The free tier is enough to see whether the idea holds up on your own numbers.&lt;/p&gt;

&lt;p&gt;What I'd most like from this crowd is the thing this domain punishes hardest: &lt;strong&gt;a number that's confidently, plausibly, quietly incorrect.&lt;/strong&gt; The interesting failures in a tool like this aren't crashes. If you find one, I want to know about it more than I want a signup — &lt;a href="mailto:admin@borderfolio.app"&gt;admin@borderfolio.app&lt;/a&gt;, or in the comments.&lt;/p&gt;

&lt;p&gt;The full arithmetic behind all four return metrics, with worked examples, is in &lt;a href="https://borderfolio.app/blog/how-to-track-portfolio-performance.html" rel="noopener noreferrer"&gt;how to track portfolio performance&lt;/a&gt;. The two-step withholding model and how each rate is resolved is in the &lt;a href="https://borderfolio.app/methodology/withholding.html" rel="noopener noreferrer"&gt;methodology&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;BorderFolio provides informational portfolio analytics only. Nothing here is investment, financial, legal or tax advice, and no figure in this article is a recommendation to buy, sell or hold any security. Illustrative figures are exactly that. Tax rules change and depend on individual circumstances — verify anything that matters with a qualified adviser in your jurisdiction.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>fintech</category>
      <category>python</category>
      <category>sideprojects</category>
    </item>
  </channel>
</rss>
