An investment dashboard with no build step: vanilla JS and Chart.js

A vanilla JS investment dashboard built in one session with Claude Opus 4.6, with no build step: the whole app is a folder you double-click. Its second half rebuilds four years of cash flows from purchase records alone, framed as a Material Requirements Planning problem and validated to the cent.


I've been told (mostly by people trying to sell me something) that my generation doesn't invest. That Gen Z would rather spend money on overpriced oat milk than put it into an index fund. And honestly, there's some truth to that. But I think the real problem is that nobody taught people my age how, and the people who try to teach us are usually just pitching their own platform.

So here's my actual advice, if you care: learn about finance, to understand it before you invest anything. Read about compound interest and portfolio theory. Read about why most actively managed funds underperform the S&P 500 over a 20-year horizon. Read about why diversification works even though it feels like hedging your bets. And then, only then, if you want to invest, do it with your own money and your own spreadsheet. You almost certainly won't beat the market1. The reason to do it anyway is that understanding what your money does is a kind of literacy, and illiteracy has costs.

1Statistically, about 90% of actively managed US large-cap funds underperformed the S&P 500 over the 15 years to mid-2024, and about 92% over 20, by S&P's own SPIVA U.S. Scorecard. The claim is uncontroversial: it's math plus fees.

Now, there's a second question that comes up in conversations with friends who've already accepted that investing is worth learning about, and it's a darker one: when you've already convinced yourself that AGI will be a reality, how do you face that future? Because if artificial general intelligence genuinely arrives (by which I mean an entity that can do your job better than you, well past a chatbot that writes emails), then the investment strategies of the last century might be irrelevant. What does portfolio diversification mean when a single technology could reshape entire sectors within a few years? What does buy and hold for 30 years mean when nobody knows what the economy looks like in 10? I don't have an answer. Nobody does. But I think about it every time I buy a stock, which is probably not the healthiest relationship with a brokerage account.

Without further ado (and I should warn you), there's not a lot to learn from this article. This is not a tutorial on how to build a dashboard, and it's definitely not financial advice. It's the story of the fifth time I tried to build a personal finance tracker, and the first time it actually worked. If you take anything from this, let it be that sometimes the right approach is the stupidest one, and that a folder you can double-click has a surprising amount of engineering value.

Timesheet26 · feb 12Read noteStarted this at noon. 22 files and a force push later, I have a dashboard and a security incident. Good Wednesday.

Attempt number five

This is the fifth time I've tried to build something like this. The previous four attempts died at various stages of ambition:

A Python CLI that got too complex

A React prototype that I abandoned when I realized I was building a framework instead of a tool

A quick-and-dirty HTML page that couldn't handle more than one CSV

MyViewer, a deprecated predecessor that was a 3,000-line monolith in a single script.js

MyViewer technically worked, but it was the kind of code that makes you close the editor and go for a walk. Un-everything.

I'd been postponing attempt number five because I was waiting for a model that could handle the specific challenges this project demands. It needed specific financial logic more than general coding:

Cross-filtering with collision rulesWhen you filter by platform and by date range simultaneously, the results need to intersect correctly. When a filter removes all data from a chart, the chart should show an empty state, not crash. When you clear one filter, the other should still be active. These sound trivial but the interaction matrix is large, and getting it wrong produces bugs that only appear in specific filter combinations

Weighted average cost basis under partial exitsIf you buy 10 shares at 100, then 5 more at 120, your average cost is 106.67. If you sell 8, your remaining cost basis is still 106.67, but the realized P&L depends on which shares you sold (FIFO, LIFO, average cost). I needed the model to understand this without me writing the formula

Consistent synthetic data generationFor testing, the model needed to generate fake transactions that never reach an impossible state, such as a negative balance or a sale of shares you don't own

Claude Opus 4.6 surprised me. I expected to need heavy steering on the financial calculations, and I did need some, particularly on how to compute mark-to-market P&L under partial liquidations1. But the model understood the architecture well. The 22-file structure and the event bus that connects its modules came from the model with minimal guidance. I mostly just described what I wanted to see on screen, and it built the wiring.

1Mark-to-market means valuing your portfolio at current market prices, as opposed to book value (what you paid). The difference between I paid 10K for these stocks and these stocks are worth 12K right now is mark-to-market P&L.

The CDN bet

The entire project is plain browser code with nothing to build or install. Chart.js is the only external dependency, loaded via CDN. Everything else, from the router to the CSV parser, is hand-written JavaScript split across clean modules.

FinBoard is a single-user tool that reads a local CSV and renders charts. It has no server logic, nothing that justifies a build pipeline. The constraint is the feature: the entire app is a folder you double-click. index.html opens in your browser with demo data loaded, and you have a dashboard. Host it on GitHub Pages or email it to yourself; it doesn't care.

The trade-off is real: no TypeScript means no compile-time safety. No SSR means no SEO. No code splitting means the whole app loads at once. But for a personal dashboard with one user and thirteen views, those are solutions to problems that don't exist. You could argue React or Vue would make the UI more maintainable, but at ~4K lines of JavaScript split across clean modules, the complexity never reaches the point where a virtual DOM or reactive state actually pays for itself.

The double-click test.

If you're building a personal tool with no backend requirements, ask yourself: can someone use this by double-clicking a file? If yes, you probably don't need a build step. If no, are you sure you need a backend? The answer is often that you only need to rethink the data flow. FinBoard passes the double-click test, and that simplicity is deliberate.

Chart.js and the wrapper that didn't want to wrap

Chart.js loaded via CDN meant no import statements: the library sits on window.Chart and you work with it directly. The plan was simple: write a charts.js wrapper module that creates and destroys every chart in one place. The rest of the app calls createLineChart(canvas, data, options) and never thinks about Chart.js internals.

The problem arrived in the first hour. Chart instances have a lifecycle: if you navigate between views and the canvas gets reused, you need to destroy the old chart before creating a new one. Miss this and you get a canvas reuse error: Chart.js refuses to draw on a canvas that already has a chart attached. The wrapper was supposed to handle this automatically, tracking instances by canvas ID and destroying them before re-creation.

It didn't. Charts leaked across view transitions as the instance tracker got out of sync with the actual DOM, and the console filled with Canvas is already in use errors. I spent a frustrating twenty minutes reading the Chart.js docs on chart.destroy() before realizing the issue was timing: the wrapper was trying to destroy charts that had already been removed from the DOM by the router's view swap.

Then it worked. The fix was to destroy charts before the router swaps the view, not after. Brief, but the kind of brief that costs you twenty minutes of your life.

Credits26 · feb 12Read noteBoth Claude Opus 4.6 and GPT-5.3-Codex helped on this. I can't imagine what the next generation of models will look like. Matt Shumer writes that he now describes what he wants and comes back four hours later to working software. And the part that strikes me most, which he says of 5.3-Codex: it makes decisions that feel like judgment, surprisingly similar to the ones you'd make yourself. Each iteration feels less like a tool and more like a colleague.

What you're looking at

The dashboard is, at its core, a bet on a single idea: your entire financial life can be described by one table. Every row is a transaction. Every column is an attribute of that transaction. Every chart and every filter is just looking at that same table from a different angle.

This is a wealth management dashboard1. You give it a CSV of your trades. It gives you back a picture of where your money is, and of how much of it you've lost to your own decisions.

1The only difference between managing 30K and 30M is the number of zeros and the quality of the furniture in the advisory office. The formulas for diversification and P&L are identical. Your spreadsheet doesn't know you're not rich.

The first chart: capital over time

Money goes in. It grows or it shrinks: that's the market's opinion, not yours. When you sell, the opinion becomes fact: the gain or loss becomes realized. That's the dashboard's portfolio-over-time area chart. An area that expands when you deposit and invest, and that wobbles up and down as the market changes its mind about what your positions are worth.

The dashboard view, built around capital over time.

Holdings: what each position is worth

A sortable table of every position, active and closed. Live prices from Yahoo Finance via a CORS proxy. Each row sets what the position cost against what it is worth now. Click any row and you drill down into the asset detail view.

The table separates active positions (things you still own) from closed ones (things you sold). Closed positions show realized P&L. Active positions show unrealized P&L that updates when prices refresh. The column headers are clickable: sort by P&L to see your winners and losers, sort by weight to see concentration risk.

Sortable table with real-time prices and P&L.

Timeline: when you did what

The timeline combines three visualizations:

A cumulative capital area chart with buy and sell bars over it

A monthly contribution heatmap

A table breaking down each month's activity

The heatmap uses color intensity to show when you were most active, useful for spotting seasonal patterns in your own behavior: heavier activity in some months, near-silence in others.

Capital and contributions, month by month.

Analysis: stratification and filtering

Four sub-tabs, each slicing the same data differently:

By platformHow much capital is deployed where. Useful if you use multiple brokers and want to see concentration

By asset typeHow the capital splits between kinds of asset. The allocation you think you have versus the allocation you actually have

By periodYearly investment flow, with the net contribution of each year

Butterfly P&LA horizontal bar chart showing unrealized P&L per asset: green bars to the right (winners), red bars to the left (losers). A visual slap that tells you exactly which picks are working and which are not

Butterfly P&L chart showing per-asset unrealized gains and losses.

Asset detail: the deep dive

Click any holding and you get the per-asset view. On top is a price chart with a green marker for each buy and a red one for each sell, placed at the exact date and price. Under it sit eight KPIs, from average cost to holding period, and the full history of the asset's transactions.

Per-asset price chart with transaction markers.

You could imagine extending this to overlay sector benchmarks, maybe even a custom weighted index of your top holdings to see if your manual picks are beating a simple DCA into an S&P 500 ETF (which, statistically speaking, they almost certainly are not), but the visualization would at least let you measure by how much you're underperforming, which is a kind of knowledge, I suppose. You could layer in dividend tracking, maybe even model reinvestment scenarios where the dividends auto-purchase more shares at the ex-date price, computing the hypothetical snowball effect against what you actually did with the cash. You could add drawdown analysis, or Monte Carlo simulations of future portfolio value based on historical volatility.

I'd go deeper into this but I have certain projects that are more bricks than bits waiting in the queue so... maybe another time.

The data trap

At some point a financial dashboard needs data that looks real, something better than Jane bought 10 AAPL, with the patterns and price ratios of a real portfolio. And the easiest source of realistic financial data is, unfortunately, real financial data. That convenience is the trap.

The trap starts with CORS. Browsers block file:// reads for security reasons, so if you double-click index.html and try to load a CSV from the same folder, the browser says no. The proper fix is python -m http.server, a one-line local server over http://localhost:8000, which browsers are happy with.

The lazy alternative is to hardcode a real-looking dataset straight into demo-data.js (the fallback the app shows when no CSV is uploaded) so index.html opens with a populated dashboard. It works. It is also exactly how real personal data ends up committed to a public repo just temporarily, and temporary has a way of becoming permanent the moment you push to remote.

In Hindsight.

A handful of real transactions reveals which brokers someone uses and how much they put into each position, more financial information than most people would ever share on purpose. The demo data shipped with this project is synthetic for exactly that reason. The lesson is blunt: never commit real personal data, even as a placeholder. Use python -m http.server and a generated dataset from the start.

And what about the money that isn't invested?

FinBoard tracks what you bought and what it's worth. It knows you own 3 shares of MSFT at an average cost of 404.34. It knows your NVDA position is up 23%. It can tell you your total deployed capital and your P&L, realized and unrealized.

But it doesn't know how the money got there.

There's no record of the salary deposit that funded the brokerage account, or of the bank transfer from your main account to a broker three days before you bought AAPL. The cash sitting in another broker, waiting to be deployed, is invisible too. FinBoard sees the purchases but not the procurement, like looking at a factory's output without knowing how the raw materials arrived.

Two CSVs existed: stock transactions and crypto transactions. Between them they held 104 transactions over four years on seven platforms, each purchase down to its date and price. The third CSV, the one that would track how the money reached the brokers, didn't exist. Nobody had ever written it down.

The missing data wasn't lost. It was latent. Every purchase implies a prior transfer. Every transfer implies a prior salary. The information was there, encoded in the purchase records themselves, waiting to be reverse-engineered.

I proposed this to Claude Code as essentially an industrial organization problem, like Material Requirements Planning from university, where you work backwards from a production schedule to compute the procurement timeline1. Except here, production is stock purchases and procurement is bank transfers. The finished goods demand becomes my purchase history. The raw materials are cash in broker accounts. The procurement schedule is the set of bank transfers I need to reconstruct. Lead times become transfer windows: 3 to 9 days before each purchase. And inventory is just the cash balance at each platform at any given moment.

1Material Requirements Planning (MRP) is a production planning algorithm from manufacturing. Given a demand schedule (what products to build and when), MRP works backward through the bill of materials to compute the procurement schedule (what raw materials to order and when), respecting lead times and inventory constraints.

MRP is O(nm)O(nm)O(nm) for nnn products and mmm time periods (not NP-hard), but the constraint propagation gets interesting once timing windows and rounding heuristics (transfers tend to be round amounts like 500, 1000, or 1850, never the exact purchase cost) meet the global invariant that no account can go negative at any point in the simulation. That last constraint is what turns it from an accounting exercise into a validation problem.

The topology is a star network: salary lands in one main account, which distributes to the brokers. All money flows through that hub, and over four years it reached seven platforms. The question is whether you can reconstruct the complete cash flow history from the purchase records alone.

The validation script

I invited Claude to invent a validation script as a kind of in-house reward signal: a function that tells you whether your reconstructed history is physically possible. A 256-line Python program that merges all three CSVs into a single chronological timeline (~170 events), then walks through it event by event. The core is deceptively simple:

PYTHON Copy

cash = defaultdict(float)

for dt, evt, sub, frm, to, amt, note in timeline: if evt == "LIQ": if sub in ("SAL", "INI"): cash[to] += amt elif sub == "TRF": if amt < 0: # outgoing transfer cash[frm] += amt cash[to] -= amt if cash[frm] < -0.01: errors.append(f"{frm} went NEGATIVE: {cash[frm]:.2f}") elif evt == "BUY": cash[plat] -= amt if cash[plat] < -0.01: errors.append(f"{plat} NEGATIVE after buying {note}") elif evt == "SEL": cash[plat] += amt

Salary deposits add money to the main account. Transfers move money between accounts. Purchases subtract from brokers. Sales add back. If any account goes negative at any point, the reconstruction is wrong: the money wasn't there when it needed to be. After the simulation, it compares computed balances against declared balances. For brokers they must match within 1 EUR. For the main account, the computed balance is always higher than declared, because the simulation doesn't account for rent or the general cost of being alive. That difference is expected. The error case is if computed is lower than declared, which would mean the income model is wrong.

Four iterations to zero errors

Iteration Errors What was wrong

1 11 No initial savings, salaries only in active months, sell proceeds double-counted

2 3 Balance mismatches: missing recent transfers

3 3 Rounded amounts didn't add up exactly: 70, 168, 260 EUR off

4 0 All balances match to the cent

Iteration 1 was a mess. The simulation started with zero money and immediately tried to spend 5,600 EUR on crypto. The main account went negative on the first transfer. Fix: add an initial savings event of 8,000 EUR. Sell proceeds were double-counted: the script already credits cash when it sees a sale, but I'd also added explicit lines in the liquidity CSV for the same proceeds. Eliminated the duplicates.

Iteration 2 was structural reform: all 50 months of salary. Only 3 errors survived: balance mismatches where recent cash transfers hadn't been recorded.

Iteration 3 added the missing transfers. The mismatches shrank from thousands to hundreds. Over 44K EUR in total transfers, residuals of 0.16%, 0.38%, and 0.59%. Pure rounding error.

Iteration 4 fine-tuned three amounts. The first Broker A transfer: 600 became 670.74. The recent Broker B funding: 4000 became 3831.17. The Broker C cash reserve: 3018 became 2757.94.

Validated to the cent

Platform Computed Declared Status

Broker A 552.20 552.20 MATCH

Broker B 4,035.72 4,035.72 MATCH

Broker C 3,018.00 3,018.00 MATCH

Main account 68,506.21 1,310.92 OK (diff = living expenses)

Three brokers match to the cent. The main account's difference, spread over the same span, is consistent with ordinary monthly living expenses: the residual lands in a believable range rather than something absurd, which is exactly the signal that the reconstruction holds together.

Of everything deposited, roughly 44K was transferred out to the platforms and 36K of it went into purchases. With 6K back from sales, about 30K is net invested.

The three CSVs now check against each other to the cent. The session ended with ~189K tokens in its context and cost about $5.13 at Opus 4.6 pricing, most of it that same context read again on every turn. The price of a mediocre coffee for a mathematically consistent reconstruction of 50 months of financial history.

Where it stands26 · feb 12Read noteWhere it stands26 · feb 12One session gave a validated reconstruction of 4 years of cash flows. The script that validates it is 256 lines of Python. The data it validates took 50 months to generate. There's something satisfying about that ratio.

A personal note26 · feb 13The frontend now includes the liquidity data too. The plan is to replace the heuristic reconstruction with real bank records, but you know how banks are with these things, so for now, the scaffold holds.

Provisional reconstruction.

This entire liquidity history is heuristic: I estimated transfer dates and amounts based on purchase patterns. The real transfer records exist in the underlying bank statements; once those are in hand, every inferred transfer gets replaced with the actual one, and the validation script will tell you exactly how close (or far) the heuristic reconstruction was from reality. Think of this as a scaffold: it holds the shape while the real structure is being built.

What this section really demonstrates is what happens when you frame a personal finance problem as an industrial engineering one and hand it to a model with the context to reason about constraints. Claude already knew MRP; it needed to be told that the problem was MRP, and once framed, the rest followed naturally. The model's main contribution was organizational reasoning: the ability to hold 170 events across 7 platforms and 50 months in working memory and identify where the numbers didn't add up.

The next step is bigger: classify every entry of a full bank-transaction history and build from it a complete financial dataset, validated to the cent, with nothing estimated. All in service of the thesis that data about your own life is the most valuable dataset you'll never be given. You have to build it yourself.