Cole PageCredit Analyst
ProjectEQ

SIF Portfolio Dashboard

A portfolio system I built for the UCLA Anderson Student Investment Fund: accounting off one transaction ledger, per-name research and notes, attribution and risk, and the weekly board report.

I built it and I run it. The screenshots here, and the demo behind them, run the same code against a synthetic fund: made-up members, a made-up book, two years of generated history. No position, person, or document on this page is real. Market data is live, so the prices are today's.

Overview. CALX has traded through the down price set on the position, which is what puts it in Action Required.
The overview tab: index levels, an action-required panel, fund metrics, and the week's attribution

Why it exists

The fund used to live in spreadsheets. Holdings in one workbook, transactions in another, performance in a third, and analyst notes wherever each analyst happened to keep them. Four problems came out of that.

Every number got re-derived by hand each week, so two people could compute the same return two different ways and both defend it. Cost basis drifted from the transaction log, because nothing forced them to agree. Nobody could say who changed a figure, when, or what it said before. And when a member graduated, whatever they knew about their names left with them.

A database solves the first three on its own: one definition of each number, one place to write things down, a record of who wrote it. The fourth needed more than storage.

The idea I took from the buy side

At the credit fund where I interned, on the leveraged finance side, every issuer we followed had a research file going back years. Underwritings, earnings reactions, event notes, all dated and attributed. Picking up a new name meant reading what the desk thought three years ago and why that changed. I came to think the file mattered more than any model in it. A model gives you someone's conclusion. The file shows you how they got there, and where they were wrong.

Student funds throw that away every spring. The class turns over, the analyst graduates, and the reasoning leaves with them. The next person inherits a ticker and a cost basis.

So notes here live on the issuer. Each one is typed (earnings reaction, news, trim, close, open question), attributed, and dated. A member joining in 2027 can open a name, read what the fund thought about it in 2025, see what happened at each print, and find out why the position got trimmed.

CALX, from the initiation in March through the July print. Every note carries its type, its author, and its date.
Three dated notes on CALX, each labelled with a type and an author

If I had to keep one half of this system I'd keep the notes. Given a custodian statement, you could rebuild the accounting in a week. Nobody can reconstruct what an analyst was thinking in 2025.

What it does

The books. FIFO lot accounting runs off the transaction ledger and reconciles to the penny. Cost basis, realized and unrealized P&L, and cash are all computed from that one ledger, so there is nothing to keep in sync. Concurrent edits are version-checked, which stops two admins from silently overwriting each other. Every mutation lands in a hash-chained audit log you can verify end to end.

The ledger, 476 rows. Every cost basis and P&L figure in the app is computed from these.
The transaction log, showing date, ticker, action, portfolio, shares, price, and cash flow

Research. Each name gets a workspace: price, fundamentals, filings, transcripts, estimates, peers, and the full note history. An analyst uploads a memo as a PDF and the thesis gets pulled out of it automatically. Price targets carry an up case and a down case, and they are versioned, so pulling up a report from March shows you March's targets.

The position and the thesis behind it. The $65 target came out of an uploaded memo; new targets are added as versions, so the old ones stay readable.
The CALX position page, showing shares, cost, P&L, the price target with up and down cases, and the thesis extracted from the source memo

Compliance. Checks run against the fund's own policy sections: sleeve drift against target, the 15% gain and loss review band, cash policy, concentration. Hitting a price target or a downside case raises its own trigger. A separate set of data-health checks watches the platform itself and says plainly when a number should not be trusted.

Performance and risk. Returns against a sleeve-weighted blended benchmark. Brinson attribution by sleeve and by individual name, plus active share, tracking error, value-at-risk, and a correlation matrix built from several years of daily security returns.

Allocation, selection, and interaction, by sector. The returns are generated, not achieved.
Brinson attribution showing portfolio and benchmark returns, the allocation, selection and interaction split, and a sector table

The board report. It writes itself out of the books every week. Because coverage assignments and price targets are time-versioned, an old week regenerates exactly as it stood at the time.

What it does not do yet

The books reconcile to the transaction ledger. They do not reconcile to a custodian statement, which means the platform can prove it is internally consistent and cannot prove it matches the broker. Weekly statement and NAV reconciliation is the next thing I would build.

Several risk analytics ship switched off. The fund has months of return history, and a Sharpe ratio computed on twelve weeks is decoration. Those turn on when the data supports them. Benchmark sector returns use a broad-market proxy instead of the actual index constituents, which holds up at the sleeve level and gets too coarse below it.

How it is built

Next.js 16, React 19, TypeScript, Postgres through Prisma, Tailwind, deployed on Vercel. Thirty-five tables, twenty member-facing tabs, eight admin. Around a thousand unit tests, run as part of the build itself. Market data comes from a commercial provider, cached per endpoint so quotes stay current without burning through the rate limit.

The constraint I designed around was accuracy, ahead of speed and ahead of how it looks. Every metric has exactly one function behind it, tested against fixtures. If two routes compute the same number two different ways, that is a bug, and it gets fixed before anything else ships.