<?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>How to Combine Portfolios From Multiple Brokers Without Connecting Your Accounts</title>
      <dc:creator>Steffan Kharmaaiarvi</dc:creator>
      <pubDate>Fri, 04 Sep 2026 16:50:54 +0000</pubDate>
      <link>https://dev.to/steffankharmaaiarvi/how-to-combine-portfolios-from-multiple-brokers-without-connecting-your-accounts-1bnp</link>
      <guid>https://dev.to/steffankharmaaiarvi/how-to-combine-portfolios-from-multiple-brokers-without-connecting-your-accounts-1bnp</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1ybrurwsql8coku3c9o9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1ybrurwsql8coku3c9o9.png" alt=" " width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  How to Combine Portfolios From Multiple Brokers Without Connecting Your Accounts
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;A practical way to build one investment history from separate broker statements—without pretending that a combined balance is enough.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For a while, I thought combining brokerage accounts was just an addition problem.&lt;/p&gt;

&lt;p&gt;Open each app. Copy the current value. Convert everything into one currency. Add the numbers together.&lt;/p&gt;

&lt;p&gt;That gives you a total. It does not give you a portfolio.&lt;/p&gt;

&lt;p&gt;My own investments ended up spread across more than one account for fairly ordinary reasons. One account held older positions. Another became the place where I made new purchases. Each broker could tell me exactly what was held on its own platform, but neither knew anything about the other.&lt;/p&gt;

&lt;p&gt;The missing answers were not exotic:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How much had I contributed across all accounts?&lt;/li&gt;
&lt;li&gt;How much came from market movement rather than new money?&lt;/li&gt;
&lt;li&gt;What was my combined cost basis?&lt;/li&gt;
&lt;li&gt;How much income had I received, and how much tax had been withheld?&lt;/li&gt;
&lt;li&gt;Was I looking at one portfolio or several unrelated dashboards?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is where portfolio consolidation becomes harder than it first appears. The problem is not collecting three balances. The problem is reconstructing one history from systems that were never designed to talk to each other.&lt;/p&gt;

&lt;h2&gt;
  
  
  A portfolio is a ledger, not a balance
&lt;/h2&gt;

&lt;p&gt;Suppose three accounts are worth $60,900 in total today. That number alone cannot tell you whether the investments performed well.&lt;/p&gt;

&lt;p&gt;An illustrative history might look like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Amount&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Net contributions&lt;/td&gt;
&lt;td&gt;$50,500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Market movement&lt;/td&gt;
&lt;td&gt;$8,700&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dividends and interest&lt;/td&gt;
&lt;td&gt;$2,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Broker fees&lt;/td&gt;
&lt;td&gt;−$180&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tax withheld&lt;/td&gt;
&lt;td&gt;−$120&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Portfolio value&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$60,900&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Someone else could have the same $60,900 balance after contributing $70,000. The current value is identical; the outcome is not.&lt;/p&gt;

&lt;p&gt;Transfers make the picture even easier to distort. If I move $20,000 of securities from Broker A to Broker B, Broker A records assets leaving and Broker B records assets arriving. At the account level, those are real events. At the combined-portfolio level, I have contributed nothing new. Treating the receiving side as a fresh investment would inflate my lifetime contributions by $20,000.&lt;/p&gt;

&lt;p&gt;The same problem appears with dividends, commissions, withholding, currency conversion and partial sales. A useful combined portfolio must preserve the events that produced the balance, not only the balance itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four ways to put the accounts together
&lt;/h2&gt;

&lt;p&gt;There is no universally best method. The right one depends on how many accounts you have, how often you trade, how much history you need and what kind of access you are comfortable granting.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Let one broker aggregate the others
&lt;/h3&gt;

&lt;p&gt;Some brokers provide their own aggregation tools. This can be convenient because the analysis lives in an institution you already use.&lt;/p&gt;

&lt;p&gt;The obvious limitation is coverage. The broker must support the other institution or account type, and the resulting view may be designed around current allocation rather than the complete transaction history you want to preserve. Employer share plans, older regional brokers and manually held investments are often the accounts that make consolidation necessary in the first place.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Use a tracker with live account connections
&lt;/h3&gt;

&lt;p&gt;Connected trackers offer the least manual work. Once authorized, they can refresh holdings and transactions without another export.&lt;/p&gt;

&lt;p&gt;It is important not to collapse every connection into “giving an app your brokerage password.” A modern OAuth flow redirects you to the financial institution, where you authenticate and authorize access. The third-party application receives permissioned access rather than your password. &lt;a href="https://plaid.com/docs/link/oauth/" rel="noopener noreferrer"&gt;Plaid’s OAuth documentation&lt;/a&gt; describes exactly this model.&lt;/p&gt;

&lt;p&gt;That is a meaningful security improvement over credential sharing. It does not remove every trade-off.&lt;/p&gt;

&lt;p&gt;A connected service still needs continuing permission to retrieve financial data. Its usefulness depends on support for your institution, the fields supplied by that institution and the depth of history available through the feed. For example, &lt;a href="https://plaid.com/docs/api/products/investments/" rel="noopener noreferrer"&gt;Plaid Investments documents up to 24 months of investment transactions&lt;/a&gt;, while its standard account endpoint uses cached balances that &lt;a href="https://plaid.com/docs/api/accounts/" rel="noopener noreferrer"&gt;typically update about once a day&lt;/a&gt; when the connection is healthy. Other providers have different limits; the point is that “connected” does not automatically mean complete or real-time.&lt;/p&gt;

&lt;p&gt;For an active investor who wants automatic daily updates, this can still be the right compromise. For somebody who checks a long-term portfolio once a month, permanent access may solve a problem they do not actually have.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Maintain a spreadsheet
&lt;/h3&gt;

&lt;p&gt;A spreadsheet is transparent, flexible and independent of any provider. For a small portfolio, it may be all you need.&lt;/p&gt;

&lt;p&gt;The workload grows with every exception. You have to standardize tickers, preserve dates, account for commissions, convert currencies, distinguish dividends from deposits, match transfers and prevent the same transaction from being pasted twice. Then you have to maintain the formulas.&lt;/p&gt;

&lt;p&gt;The spreadsheet does not usually fail in a dramatic way. It becomes less trustworthy gradually. One missing dividend, one overwritten formula or one transfer recorded as a contribution can produce a plausible result that is still wrong.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Import statements periodically
&lt;/h3&gt;

&lt;p&gt;The fourth option is to treat each broker statement as a source document and build the combined ledger from those documents.&lt;/p&gt;

&lt;p&gt;This is less automatic than a live connection, but it is not an improvised workaround. Broker reports can contain a surprisingly complete record. Interactive Brokers says its Activity Statements include positions, cash balances, fees, corporate actions, interest, dividends and trades. Its reporting tools also allow investors to create configurable CSV, XML and text exports through Flex Queries. &lt;a href="https://www.interactivebrokers.com/campus/trading-lessons/client-portal-reporting/" rel="noopener noreferrer"&gt;IBKR documents the available reporting fields here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;DEGIRO similarly provides separate account, transaction and portfolio reports, with exports available in Excel, PDF and CSV formats. Its &lt;a href="https://www.degiro.ie/helpdesk/tax/which-reports-are-there-and-where-can-i-find-them" rel="noopener noreferrer"&gt;reporting documentation&lt;/a&gt; makes an important distinction: a portfolio overview is a snapshot of open positions, while account and transaction statements contain the activity behind those positions.&lt;/p&gt;

&lt;p&gt;That distinction matters. A holdings screenshot can tell a tracker what you own today. It usually cannot reconstruct when you bought it, what you paid, what income it produced or how much of the current value came from your own contributions.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the combined record actually needs
&lt;/h2&gt;

&lt;p&gt;If I were evaluating any multi-broker tracker, I would look for more than a clean dashboard. I would want to know whether it preserves the following:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The source account.&lt;/strong&gt; Every transaction should retain its origin. Combining accounts should not erase the ability to trace a number back to a broker statement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stable instrument identification.&lt;/strong&gt; A ticker alone is not always enough. The same security can trade in different currencies or on different exchanges, and the same short ticker can refer to different instruments. ISINs and market identifiers help disambiguate them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dated external cash flows.&lt;/strong&gt; Contributions and withdrawals need dates and amounts. Without them, money-weighted return is guesswork.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Internal transfers.&lt;/strong&gt; Moving cash or securities between my own accounts should not become a new contribution to the combined portfolio.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Income and costs.&lt;/strong&gt; Dividends, interest, commissions and withholding should remain separate. Folding everything into an unexplained gain hides exactly the leakage many investors are trying to understand.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Historical currency conversion.&lt;/strong&gt; A contribution made in euros should retain its value at the exchange rate on the transaction date. Re-converting the old contribution at today’s rate silently rewrites history.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Duplicate detection.&lt;/strong&gt; Overlapping statements are normal. I may export January through August in September and January through September in October. A safe importer should recognize previously recorded transactions before it changes the portfolio.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A review step.&lt;/strong&gt; PDF layouts vary, CSV columns differ and corporate actions can be ambiguous. Extraction should be treated as a draft to verify, not as unquestionable truth.&lt;/p&gt;

&lt;h2&gt;
  
  
  The reconciliation test
&lt;/h2&gt;

&lt;p&gt;After consolidation, the portfolio should pass a basic accounting identity:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Opening value + net contributions + market movement + income − fees − tax withheld = ending value&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;$42,000 + $7,000 + $3,100 + $612 − $84 − $168 = $52,460&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If the two sides do not match, something is missing, duplicated or classified incorrectly. That disagreement is useful information. A tracker that simply assigns every unexplained difference to “market return” will always balance, but it cannot tell you whether the result is correct.&lt;/p&gt;

&lt;p&gt;Performance requires another distinction. The return percentages shown by three brokers cannot simply be averaged. Contributions occurred at different times and in different amounts. Time-weighted return attempts to remove the effect of external cash flows so the underlying portfolio can be compared across periods; money-weighted return reflects the investor’s actual timing and size of those flows. Even professional performance standards require explicit policies for external cash flows and generally use time-weighted returns for comparability. The &lt;a href="https://www.cfainstitute.org/insights/professional-learning/refresher-readings/2026/overview-of-the-global-investment-performance-standards" rel="noopener noreferrer"&gt;CFA Institute’s overview of the GIPS standards&lt;/a&gt; explains why this treatment matters.&lt;/p&gt;

&lt;p&gt;For a personal portfolio, both views are useful. They answer different questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What did the portfolio do?&lt;/strong&gt; Time-weighted return.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What happened to my money?&lt;/strong&gt; Money-weighted return, often calculated as XIRR.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Neither is trustworthy without a sufficiently complete ledger.&lt;/p&gt;

&lt;h2&gt;
  
  
  “No broker connection” does not mean “no privacy decision”
&lt;/h2&gt;

&lt;p&gt;A statement-based workflow removes one kind of access: the tracker has no standing authorization to query the broker, and it cannot use a broker connection to place trades or move money.&lt;/p&gt;

&lt;p&gt;But a statement is still a sensitive financial document. It may contain a name, address, account number, holdings and transaction history. Uploading it requires a different set of questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is the original file stored, and for how long?&lt;/li&gt;
&lt;li&gt;Is the document sent to another processor for extraction?&lt;/li&gt;
&lt;li&gt;Which fields are retained after parsing?&lt;/li&gt;
&lt;li&gt;Can I review the extracted data before saving it?&lt;/li&gt;
&lt;li&gt;Can I delete the portfolio and account myself?&lt;/li&gt;
&lt;li&gt;Does the provider have an independent security certification or penetration test?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The right comparison is not “connected equals dangerous, document equals safe.” The right comparison is the exact access, retention, processing and deletion model of each service.&lt;/p&gt;

&lt;p&gt;OAuth can be a sensible choice when it is implemented well and the institution supports it. Statement import can be a sensible choice when you want broad compatibility and periodic control. Manual entry remains the lowest-data-sharing option, at the cost of the most work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The approach I ended up building
&lt;/h2&gt;

&lt;p&gt;I ran into this problem while trying to maintain one coherent history across accounts. I wanted the combined analysis, but I did not need another service continuously connected to my brokerage accounts. That is why I ended up building BorderFolio around a document-in workflow.&lt;/p&gt;

&lt;p&gt;Each broker or account can remain a separate portfolio, while the combined view brings them together. BorderFolio accepts PDF and CSV statements, as well as screenshots or photos when no useful export exists. It extracts positions and transactions, shows the result for review and skips transactions it has already imported. It never asks for broker credentials.&lt;/p&gt;

&lt;p&gt;There is a trade-off, and it should be stated plainly. Arbitrary statement layouts are extracted using the OpenAI API, so the contents of an uploaded document are sent to that processor. BorderFolio keeps the original file in private storage only so a failed import can be retried and deletes it automatically within seven days. The product is still young: it does not have SOC 2 or ISO 27001 certification or a third-party penetration test. Manual entry is available for anyone who does not want to upload a statement. The complete details are published on the &lt;a href="https://borderfolio.app/security.html?utm_source=medium&amp;amp;utm_medium=article&amp;amp;utm_campaign=outreach" rel="noopener noreferrer"&gt;security and data-handling page&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I do not think this approach replaces connected trackers. If you trade frequently and want intraday updates, a reliable API connection is probably more convenient. If you need jurisdiction-specific tax filing, a portfolio tracker is not a substitute for tax software or an accountant. And if you have one account with five holdings, a spreadsheet may remain perfectly adequate.&lt;/p&gt;

&lt;p&gt;Statement import is most useful for the awkward middle: several brokers, long holding periods, regular contributions and enough history that starting from today would throw away the part you actually want to understand.&lt;/p&gt;

&lt;h2&gt;
  
  
  The dashboard is the last step
&lt;/h2&gt;

&lt;p&gt;Combining portfolios is often presented as a visualization problem. Put every account on one screen, draw an allocation chart and show a total balance.&lt;/p&gt;

&lt;p&gt;The dashboard is the easy part.&lt;/p&gt;

&lt;p&gt;The real product is the ledger underneath it: where every position came from, when money entered, what moved internally, which income was paid, which costs were taken and whether the pieces add up.&lt;/p&gt;

&lt;p&gt;The best multi-broker setup is not necessarily the one with the most automation. It is the one whose history you can explain.&lt;/p&gt;

&lt;p&gt;If a statement-based approach fits the way you invest, you can &lt;a href="https://borderfolio.app/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=outreach" rel="noopener noreferrer"&gt;try BorderFolio with one portfolio for free&lt;/a&gt;. If your broker’s format does not import correctly, that is exactly the kind of feedback I am looking for.&lt;/p&gt;




&lt;h3&gt;
  
  
  Sources and further reading
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.interactivebrokers.com/campus/trading-lessons/client-portal-reporting/" rel="noopener noreferrer"&gt;Interactive Brokers: Client Portal reporting and Flex Queries&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.degiro.ie/helpdesk/tax/which-reports-are-there-and-where-can-i-find-them" rel="noopener noreferrer"&gt;DEGIRO: account, transaction and portfolio reports&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://plaid.com/docs/link/oauth/" rel="noopener noreferrer"&gt;Plaid: how OAuth-based financial account authorization works&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://plaid.com/docs/api/products/investments/" rel="noopener noreferrer"&gt;Plaid: Investments API history and fields&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.cfainstitute.org/insights/professional-learning/refresher-readings/2026/overview-of-the-global-investment-performance-standards" rel="noopener noreferrer"&gt;CFA Institute: overview of the Global Investment Performance Standards&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://borderfolio.app/broker-statement-import.html?utm_source=medium&amp;amp;utm_medium=article&amp;amp;utm_campaign=outreach" rel="noopener noreferrer"&gt;BorderFolio: broker statement import methodology&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Sources and product details checked on 4 September 2026. This article is informational only and is not investment, tax or legal advice.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>investing</category>
      <category>fintech</category>
      <category>personalfinance</category>
      <category>saas</category>
    </item>
    <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>
