Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/assets/banner.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions .github/assets/icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/screenshots/card.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/screenshots/options.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/screenshots/popup.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
node_modules/
dist/
release/
*.local
.DS_Store
*.har
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ These are the rules we hold the line on in this repo. Apply them to every change
## Commands

```bash
npm run build # build all 3 Vite configs + copy manifest/styles → dist/
npm run build # single `vite build` via @crxjs/vite-plugin → dist/ (Chrome-ready MV3)
npm run test # vitest run (node env)
npm run typecheck # tsc --noEmit (both tsconfigs)
npm run lint # eslint src
Expand Down
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 David Rogers

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
156 changes: 156 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
<p align="center">
<img src=".github/assets/banner.svg" alt="SectorScope" width="640">
</p>

<p align="center">
<em>A Chrome extension that adds a native-looking sector-exposure card to your Fidelity portfolio. Recommendation-only; all data stays local.</em>
</p>

<p align="center">
<a href="https://github.com/drogers0/SectorScope/actions/workflows/ci.yml"><img src="https://github.com/drogers0/SectorScope/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
<a href="LICENSE"><img src="https://img.shields.io/github/license/drogers0/SectorScope?color=lightgrey" alt="License: MIT"></a>
<img src="https://img.shields.io/badge/manifest-v3-2b7?logo=googlechrome&logoColor=white" alt="Manifest V3">
<img src="https://img.shields.io/badge/built%20with-TypeScript-3178c6?logo=typescript&logoColor=white" alt="TypeScript">
<img src="https://img.shields.io/badge/tests-416%20passing-2b7" alt="Tests">
</p>

---

Fidelity's portfolio summary shows you an **asset allocation** donut (stocks vs. bonds vs. cash), but nothing about your **sector** mix. SectorScope is a Chrome extension (Manifest V3) that adds a card looking like it shipped with the site: it reads the holdings already on the page, resolves each fund/ETF to its true sector breakdown, and charts your real Information-Technology-vs-Financials-vs-everything-else exposure. Then, against a target allocation you set, it tells you **what to buy with your available cash** to close the gaps, and optionally what to trim.

It never asks for your credentials, never places a trade, and never sends your holdings anywhere. Everything runs in your browser.

> [!NOTE]
> SectorScope is **recommendation-only** and provides **estimates**. It does not place, pre-fill, or submit trades, and it is **not affiliated with or endorsed by Fidelity Investments**.

<p align="center">
<img src=".github/screenshots/card.png" alt="The SectorScope card injected on the Fidelity summary page" width="440">
</p>

## Features

- 🍩 **Native-looking sector donut** — a custom SVG chart (no CDN libraries) styled to match Fidelity's own Asset allocation card, injected right into the Summary page's masonry layout.
- 🧮 **True look-through exposure** — resolves each holding to its underlying sector mix via Fidelity's ETF and mutual-fund composition endpoints (by symbol and CUSIP), so a total-market fund is split across sectors instead of dumped into "Other."
- 🎯 **Target allocation you control** — set a per-sector target (per Fidelity account), map each sector to a preferred buy ticker, and see current-vs-target at a glance.
- 💸 **Holdings-aware buy recommendations** — a greedy allocator deploys your *available-to-trade* cash into the most underweight sectors, preferring instruments you already hold before introducing a new ticker.
- ✂️ **Optional trim suggestions** — sector-level sell-to-target amounts for overweight sectors, restrictable to tax-advantaged accounts so you don't trigger avoidable capital gains.
- 🔒 **Local by design** — target allocations and a short-lived sector-exposure cache live in `chrome.storage.local`. No account data ever leaves the browser.

## Screenshots

<table>
<tr>
<th width="38%">Toolbar popup — quick per-account glance & toggles</th>
<th width="62%">Options — per-account targets, buy tickers, and behavior</th>
</tr>
<tr>
<td valign="top"><img src=".github/screenshots/popup.png" alt="SectorScope popup"></td>
<td valign="top"><img src=".github/screenshots/options.png" alt="SectorScope options page"></td>
</tr>
</table>

> All screenshots use **synthetic** holdings and made-up tickers (`TCHGX`, `INDUX`, …) — never real account data.

## How it works

```mermaid
flowchart LR
A([Fidelity Summary page]) --> B[Content script<br/>reads holdings + cash]
B --> C[Service worker<br/>fetches sector composition]
C -. opt-in .-> Y[Yahoo lookup<br/>unclassified funds]
Y --> D
C --> D[Aggregate → sector %]
T[(Target allocation<br/>chrome.storage.local)] --> D
D --> E[Allocate cash to<br/>underweight sectors]
E --> F([Render card<br/>donut · buys · trims])

classDef proc fill:#1b2438,stroke:#28324d,color:#e6e9ef;
classDef edge fill:#243056,stroke:#6c7ba6,color:#c9cfdb;
classDef opt fill:#202a3f,stroke:#6c7ba6,color:#9aa4b7,stroke-dasharray:4 3;
classDef out fill:#ef5f6e,stroke:#c94656,color:#20131a,font-weight:bold;
class B,C,D,E proc;
class A,T edge;
class Y opt;
class F out;
linkStyle default stroke:#6c7ba6,stroke-width:1.5px;
```

Every step runs locally. The only network requests are the sector-composition lookups described in [Privacy & permissions](#privacy--permissions) — never your holdings, balances, or account numbers.

## Privacy & permissions

SectorScope handles private financial data, so it is built to keep that data on your machine:

- **No credentials.** It never asks for, reads, or stores your Fidelity username, password, cookies, or CSRF tokens.
- **No trading.** It cannot place, pre-fill, or submit orders. It only reads and recommends.
- **No analytics, no backend.** There is no server. Your holdings, balances, and account numbers are never transmitted anywhere.
- **Local storage only.** Your target allocations and a 7-day sector-exposure cache live in `chrome.storage.local`. Account identifiers are hashed before use.

Every permission is requested for one concrete reason:

| Permission | Why it's needed |
| --- | --- |
| `storage` | Save your target allocations and cache sector-exposure data locally. |
| `digital.fidelity.com` | Read the holdings and available-to-trade balance from the portfolio page you're viewing, and inject the card. |
| `research2relay.fidelity.wallst.com` | Fetch **ETF** sector exposure by symbol. |
| `fundresearch.fidelity.com` | Fetch **mutual-fund** composition by CUSIP. |
| `query1.finance.yahoo.com`, `fc.yahoo.com` | **Opt-in only** — look up sector data for funds Fidelity doesn't classify (e.g. some 401(k)/403(b) institutional funds). Off by default; enable it in Settings. |

All Fidelity fetches are made with `credentials: 'omit'` and an 8-second timeout.

## Install

SectorScope isn't on the Chrome Web Store yet. A prebuilt, ready-to-load zip is attached to every [GitHub Release](https://github.com/drogers0/SectorScope/releases/latest) — no build tools required.

1. Download `sectorscope-dist-v<version>.zip` from the [latest release](https://github.com/drogers0/SectorScope/releases/latest).
2. Unzip it. You'll get a `sectorscope-<version>/` folder that contains `manifest.json`.
3. Open `chrome://extensions`, enable **Developer mode** (top-right), click **Load unpacked**, and select that unzipped `sectorscope-<version>/` folder — the one *containing* `manifest.json`, not its parent.

Then log in to Fidelity and open **Portfolio → Summary**. The card injects below the Asset allocation card. If it asks you to visit your Positions page once, do that and return to Summary (Positions primes the holdings data). The MV3 service worker shows **"Inactive"** on `chrome://extensions` until a Fidelity page wakes it — that's normal.


### Build it yourself

Prefer to build from source? Requires **Node 18+**:

```bash
git clone https://github.com/drogers0/SectorScope.git
cd SectorScope
npm install
npm run build # outputs the loadable extension to dist/
```

Then **Load unpacked** the resulting **`dist/`** folder (the one containing `manifest.json`) exactly as above.

## Development

```bash
npm run build # build all Vite configs + copy manifest/icons/styles → dist/
npm run test # vitest (node env) — 416 tests
npm run typecheck # tsc --noEmit (both tsconfigs)
npm run lint # eslint src
```

**Architecture:**

- `src/content/` — content script: detects the page, reads holdings/balances, injects the card, survives SPA navigation.
- `src/background/` — MV3 service worker: cross-origin sector-exposure fetches + cache.
- `src/core/` — framework-free engine: sector mapping, aggregation, and the buy/trim recommendation logic (the primary unit-test surface).
- `src/ui/` — the vanilla-TS card island (donut, legend, tables).
- `src/popup/`, `src/options/` — React surfaces for quick toggles and the target-allocation editor.
- `src/fidelity/` — parsers for Fidelity's positions/balances/account endpoints, driven by sanitized fixtures.

> [!IMPORTANT]
> Unit tests run in a node env with no DOM, no service worker, and no live Fidelity endpoints. Passing `typecheck` / `lint` / `test` is necessary but **not sufficient** — build, load the unpacked `dist/` in a Chrome dev window, and verify the change on a real Fidelity page before calling it done.
>
> **Never commit real account data.** Tests, fixtures, and screenshots must use synthetic values only.

## Contributing

Issues and pull requests are welcome. Please keep changes covered by tests, run `npm run typecheck && npm run lint && npm run test` before opening a PR, and never include real brokerage data in code, fixtures, screenshots, or commit messages.

## License

[MIT](LICENSE) © David Rogers

<p align="center"><sub>SectorScope provides estimates and does not place trades. Not affiliated with Fidelity Investments.</sub></p>
Binary file added icons/icon-128.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added icons/icon-16.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added icons/icon-32.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added icons/icon-48.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
22 changes: 17 additions & 5 deletions manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
"version": "1.0.0",
"description": "Augments Fidelity portfolio pages with a native-looking Sector exposure card. Recommendation-only. All data stays local.",
"permissions": ["storage"],
"icons": {
"16": "icons/icon-16.png",
"32": "icons/icon-32.png",
"48": "icons/icon-48.png",
"128": "icons/icon-128.png"
},
"host_permissions": [
"https://digital.fidelity.com/*",
"https://research2relay.fidelity.wallst.com/*",
Expand All @@ -12,26 +18,32 @@
"https://fc.yahoo.com/*"
],
"background": {
"service_worker": "serviceWorker.js",
"service_worker": "src/background/serviceWorker.ts",
"type": "module"
},
"content_scripts": [
{
"matches": ["https://digital.fidelity.com/*"],
"js": ["pageDataCapture.js"],
"js": ["src/content/pageDataCapture.iife.ts"],
"run_at": "document_start",
"world": "MAIN"
},
{
"matches": ["https://digital.fidelity.com/*"],
"js": ["contentScript.js"],
"css": ["styles.css"],
"js": ["src/content/contentScript.iife.ts"],
"css": ["src/content/styles.css"],
"run_at": "document_start"
}
],
"action": {
"default_popup": "popup.html",
"default_title": "SectorScope"
"default_title": "SectorScope",
"default_icon": {
"16": "icons/icon-16.png",
"32": "icons/icon-32.png",
"48": "icons/icon-48.png",
"128": "icons/icon-128.png"
}
},
"options_ui": {
"page": "options.html",
Expand Down
Loading
Loading