Fetching quotes...
๐Ÿ“Š

Sign In

Enter your credentials to access your portfolio

Don't have an account? Create one

First time? Set up Supabase connection

You'll find these in your Supabase project โ†’ Settings โ†’ API. The publishable key is safe for client-side use โ€” Row Level Security protects your data.

Portfolio Dashboard

Set up your stock data provider, and optionally sync holdings across devices via Google Sheets.

Stock Price Provider

Google Sheet Sync (optional โ€” for cross-device access)

This lets your trades and holdings sync across any browser/phone. Skip this to use local-only storage.

One-time setup instructions (5 min)
  1. Upload Portfolio Holdings.xlsx to Google Drive โ†’ right-click โ†’ Open with Google Sheets
  2. In Google Sheets, go to Extensions โ†’ Apps Script
  3. Delete any code already there, then paste the script below:
click to copyfunction doGet(e) { var ss = SpreadsheetApp.getActiveSpreadsheet(); var sheet = ss.getSheetByName("Holdings"); var data = sheet.getDataRange().getValues(); var holdings = []; for (var i = 1; i < data.length; i++) { if (data[i][0] && data[i][2] > 0) { holdings.push({t:data[i][0], n:data[i][1], s:data[i][2], c:data[i][3], sec:data[i][4]||"Other", cat:data[i][5]||""}); } } // Read extras and history from Config sheet var extras = {}, history = []; var cfg = ss.getSheetByName("Config"); if (cfg) { try { extras = JSON.parse(cfg.getRange("A2").getValue() || "{}"); } catch(ex) {} try { history = JSON.parse(cfg.getRange("B2").getValue() || "[]"); } catch(ex) {} } return ContentService.createTextOutput(JSON.stringify({holdings:holdings, extras:extras, history:history})).setMimeType(ContentService.MimeType.JSON); } function doPost(e) { var ss = SpreadsheetApp.getActiveSpreadsheet(); var sheet = ss.getSheetByName("Holdings"); var payload = JSON.parse(e.postData.contents); var h = payload.holdings; // Clear old data, keep header var last = sheet.getLastRow(); if (last > 1) sheet.getRange(2, 1, last - 1, 6).clearContent(); // Write updated holdings for (var i = 0; i < h.length; i++) { sheet.getRange(i + 2, 1, 1, 6).setValues([[h[i].t, h[i].n, h[i].s, h[i].c, h[i].sec, h[i].cat||""]]); } // Save extras and history to Config sheet var cfg = ss.getSheetByName("Config"); if (!cfg) { cfg = ss.insertSheet("Config"); cfg.getRange("A1").setValue("Extras JSON"); cfg.getRange("B1").setValue("History JSON"); } if (payload.extras) { cfg.getRange("A2").setValue(JSON.stringify(payload.extras)); } if (payload.history) { cfg.getRange("B2").setValue(JSON.stringify(payload.history)); } // Log trade if included if (payload.trade) { var log = ss.getSheetByName("Trade Log"); if (log) { var lr = Math.max(log.getLastRow() + 1, 4); log.getRange(lr, 1, 1, 7).setValues([[ new Date().toISOString().slice(0,10), payload.trade.ticker, payload.trade.action, payload.trade.shares, payload.trade.price, payload.trade.shares * payload.trade.price, payload.trade.raw || "" ]]); } } return ContentService.createTextOutput(JSON.stringify({ok:true})).setMimeType(ContentService.MimeType.JSON); }
  1. Click Deploy โ†’ New deployment
  2. Type = Web app, Execute as = Me, Who has access = Anyone
  3. Click Deploy, authorize when prompted, then copy the Web app URL

"Anyone" means anyone with the URL โ€” it's like a private link. The URL is long and random, so only you will have it.

Stock API key and sync URL are saved in your browser. Holdings sync to Google Sheet if connected, or stay in local storage.

Portfolio Dashboard ๐ŸŒธ๐Ÿข๐ŸŒผ๐Ÿ‡๐ŸŒท v3.21

Loading market data...
Supabase Local only

Add Trade

Paste one or more Robinhood trade confirmations (one per line for bulk). Examples:
"Cameco limit buy ยท Individual ยท Mar 20 ยท $6,031.63 ยท 59 shares at $102.23"
"TSLA market sell 10 shares at $250.50"
"Buy 100 NVDA at $130.00"
Paste multiple lines at once for bulk entry!

Add Holdings

Add holdings directly (no trade history). Enter one at a time or bulk-paste multiple.


Bulk Paste

Paste multiple holdings, one per line. Format: TICKER, Name, Shares, AvgCost, Sector
Example: AAPL, Apple Inc, 50, 145.00, Technology

๐Ÿ“

Import Holdings File
Drop a CSV or XLSX file here, or click to browse.
Expected columns: Ticker, Name, Shares, Avg Cost, Sector
This will replace all current holdings.

When unchecked: replaces all holdings (legacy behavior, requires cost basis in CSV)

Cash, Crypto & Margin

โ–พ
Uninvested cash in your brokerage
Value will show after price fetch
Your average purchase price per BTC
Total value of other crypto holdings
Current margin balance (subtracted from net portfolio value)

๐Ÿ“ง Email Notifications

Settings are stored in Supabase and read by the portfolio-emailer worker on every cron trigger.

Master switch
Worker URL is stored locally in your browser (used for the "Send test email" button).
The shared secret you set with wrangler secret put TRIGGER_SECRET.
Big mover alerts (hourly during market hours)
Daily summaries
Server-side quote proxy (optional)
When enabled, the dashboard fetches quotes from your worker instead of calling FMP directly. Requires the worker to have the FMP and Finnhub keys configured as secrets. Worker URL must be set above.

What's new โ€” Build history

Activity History

$0.00
 

Sector Performance Today

โ–พ

Portfolio Breakdown

โ–พ
Today's P&L by Sector
By Sector
By Investment Category

Top Gainers & Losers

โ–พ

Top 10 Holdings by Market Value

โ–พ

Biggest Movers โ€” What's Driving Them

โ–พ

All Holdings

โ–พ
Sector:
Category:
Not financial advice. Prices may be delayed depending on your API plan. ยท Reset to default holdings