{"id":51065700,"url":"https://github.com/aliasaria/investment-tracker","last_synced_at":"2026-06-23T06:30:46.930Z","repository":{"id":360783569,"uuid":"1249466240","full_name":"aliasaria/investment-tracker","owner":"aliasaria","description":null,"archived":false,"fork":false,"pushed_at":"2026-06-12T01:05:10.000Z","size":960,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-12T03:06:12.935Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"agpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/aliasaria.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-05-25T18:22:51.000Z","updated_at":"2026-06-12T01:05:14.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/aliasaria/investment-tracker","commit_stats":null,"previous_names":["aliasaria/investment-tracker"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/aliasaria/investment-tracker","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aliasaria%2Finvestment-tracker","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aliasaria%2Finvestment-tracker/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aliasaria%2Finvestment-tracker/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aliasaria%2Finvestment-tracker/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/aliasaria","download_url":"https://codeload.github.com/aliasaria/investment-tracker/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aliasaria%2Finvestment-tracker/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34678932,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-23T02:00:07.161Z","response_time":65,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-23T06:30:43.389Z","updated_at":"2026-06-23T06:30:46.920Z","avatar_url":"https://github.com/aliasaria.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Investment Tracker\n\n![Portfolio vs index comparison](img/main.png)\n\n![Activity view](img/activity.png)\n\nA small local web app to track a multi-account **RBC Dominion Securities** portfolio against market indexes, honestly accounting for deposits and withdrawals. It only ingests RBC Dominion Securities CSV exports and PDF statements — no other broker is supported.\n\n## Features\n\n- **Works with RBC Dominion Securities** — built around the CSV exports and monthly PDF statements their site produces, no manual reshaping required.\n- **Direct upload of broker CSVs or PDF statements** — drop in **Holdings** snapshots (positions and balances per account), **Activity** statements (deposits, withdrawals, dividends, internal transfers), or monthly **PDF statements** that bundle both. The file type is auto-detected and ingested; sources are idempotent so the same period from CSV and PDF can be uploaded without duplicating.\n- **Multi-account tracking** — every account (RRSP, TFSA, non-registered, joint, etc.) is tracked separately and summed to a portfolio total.\n- **Honest index comparison via parallel-portfolio simulation** — compares against S\u0026P 500, NASDAQ, S\u0026P/TSX, and Dow Jones by simulating an index portfolio that \"buys\" and \"sells\" on every external cash flow you made. The result is a direct dollar-for-dollar line on the same chart, so deposits and withdrawals don't fake out the comparison the way percentage-return charts do.\n- **Per-account drilldown** — click \"drill in\" beside any account to rescope the index comparison to that account alone (internal transfers in/out of that account are then treated as external cash flows).\n- **USD ↔ CAD FX handling** — USD trades are converted using the rate embedded in the broker's description line when present, falling back to a daily Yahoo Finance USDCAD quote.\n- **Cash flow classification** — every activity row is tagged (external in/out, internal transfer, income, trade, fx, other). Unrecognized rows surface in a dedicated panel so nothing silently vanishes from the simulation.\n- **Data freshness diagnostics** — a freshness strip, an activity-coverage histogram, and gap warnings call out stale holdings, missing months, or activity entries that postdate your last holdings snapshot.\n- **Local-first storage** — everything lives in a single SQLite file (`investments.db`); every raw upload is archived under `uploads/archive/YYYY/MM/` for auditability.\n- **Chart annotations at cash flow dates** — vertical markers on the chart; hover for the amount, direction, and account.\n- **Agent-friendly** — because the data is just a local SQLite database, you can point an agentic harness (e.g. Claude Code) at `investments.db` and ask freeform questions about your portfolio — performance per account, fees over time, dividend yield by year, etc. — without writing a query layer yourself.\n\n## Running\n\n```sh\nnpm install\nnpm start\n```\n\nOpens http://localhost:3000 in your browser.\n\n## Usage\n\nThe tool is fed by files you export from RBC Dominion Securities — there is no manual data entry and no integration. You have two paths: live CSV exports (best for ongoing weekly/monthly updates) or monthly PDF statements (best for backfilling history). Mix freely — the database is keyed so duplicates across sources merge.\n\n### Option A — CSV exports (ongoing updates)\n\nLog in to the RBC Dominion Securities site and grab two exports:\n\n- **Holdings CSV** — go to **Account Holdings** (the page that lists positions per account). Use the export / download button on that page. This is a snapshot of *what you own* and what it's worth, on the day you export it.\n- **Activity CSV** — go to **Activity** (the page that lists deposits, withdrawals, trades, dividends, transfers). Use the export / download button on that page. Choose as wide a date range as the site offers; the ingester is idempotent so overlapping ranges are fine to re-upload later.\n\nThe export buttons live in slightly different spots depending on which version of the DS site you're on, but each page has one.\n\n### Option B — Monthly PDF statements (historical backfill)\n\nEach RBC monthly PDF statement contains both the holdings snapshot (as of month-end) and that month's activity — equivalent to one Holdings + Activity CSV pair per account per month. PDFs are the fastest way to seed years of history that predate your CSV exports.\n\nTo download them from RBC DS:\n\n1. Click **Documents** in the top navigation.\n2. Click **Account Documents**.\n3. Click **Statements**.\n4. Pick an account, filter by date range, and download each monthly PDF.\n5. **Repeat for each account** — RBC scopes statements to one account at a time. If you have RRSP, TFSA, joint, etc., download statements for each separately.\n\nThe parser handles all the RBC statement formats observed so far: A+ managed accounts (`A + STATEMENT`), basic accounts (`ACCOUNT STATEMENT`), and PIM registered accounts (TFSA, RRSP, RIF, FHSA). PDFs that bundle a CAD and a USD sub-statement are split automatically; USD is converted to CAD using the FX rate printed on the statement. Cash positions (which sit in the statement's CASH BALANCE section, not its asset review) are captured as `CASH`-type holdings.\n\n### Upload into the app\n\nDrop your files — CSV or PDF, one or many — into the upload area. The file type is auto-detected from content (PDF magic bytes vs. CSV headers), so no labelling is required. Re-uploading the same file is harmless: duplicates are skipped via unique-index constraints. Account numbers from CSV (digit-only) and PDF (hyphenated) are normalized to the same canonical form, so the same account from either source is treated as one.\n\n### Why both kinds of data are needed\n\nWhether sourced from two CSVs or from one PDF (which bundles both), the app needs holdings and activity together to do its main job:\n\n- **Holdings** tells you *what your portfolio is worth right now*. Without it, the app has no portfolio value to plot.\n- **Activity** tells you *every dollar that entered or left the portfolio*, plus dividends, trades, and internal transfers. Without it, the parallel-portfolio index comparison can't be honest: it wouldn't know when you deposited or withdrew, so a chart against the S\u0026P 500 would silently treat your contributions as portfolio gains (or your withdrawals as losses).\n\nTogether they let the app draw your real portfolio value over time *and* a same-scale \"what if I'd put each of those cash flows into the index instead\" line — dollar for dollar on the same chart.\n\n### Re-upload over time to see the trend\n\nA single upload is mostly diagnostic — you'll see your current portfolio value and a list of your cash flows, but there's no trend yet. The tool gets useful when you re-export and re-upload **on a recurring basis** (e.g. once a week or once a month). Each new holdings snapshot becomes another point on the portfolio value chart, and each new activity export fills in any cash flows since the last one. After a few cycles you'll have a real time series of your portfolio against the indexes, with deposits and withdrawals correctly accounted for.\n\nFor historical depth, batch-uploading years of monthly PDF statements is the fastest path to a fully populated chart — each PDF contributes one snapshot date per account.\n\n## Tests\n\n```sh\nnpm test\n```\n\n## Architecture\n\n- `server.js` — express bootstrap, mounts routes.\n- `db.js` — SQLite connection + schema.\n- `lib/` — pure logic units, each unit-tested:\n  - `account-id.js`, `csv-detect.js`, `file-detect.js`, `classify.js`, `fx.js`, `archive.js`,\n  - `holdings-ingest.js`, `activity-ingest.js`,\n  - `pdf-extract.js`, `pdf-statement-split.js`, `pdf-statement-parse.js`, `pdf-ingest.js`,\n  - `simulator.js`, `freshness.js`,\n  - `migrate-account-id.js`, `normalize-account-id.js` (one-time migrations)\n- `routes/` — express routers, one file per concern.\n- `test/` — `node --test` test suites.\n\n## Cash flow classification\n\nActivity rows are classified as one of:\n\n| Classification | Meaning | Used by simulator? |\n|---|---|---|\n| `external_out` | Money left the portfolio (withdrawal, wire, EFT) | yes |\n| `external_in` | Money entered the portfolio | yes |\n| `internal_transfer` | Move between two tracked accounts (e.g. spousal RRSP contribution) | per-account scope only; whole-portfolio ignores |\n| `income` | Dividends and interest received | no (already reflected in holdings totals) |\n| `trade` | Intra-portfolio Buy/Sell — no net cash effect | no |\n| `other` | Anything not yet recognized | no; visible in the \"Unrecognized activity\" UI panel |\n\nTo refine: add more rules to `lib/classify.js` and a test in `test/classify.test.js`.\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faliasaria%2Finvestment-tracker","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Faliasaria%2Finvestment-tracker","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faliasaria%2Finvestment-tracker/lists"}