A MagicMirror² module for displaying consolidated financial holdings with real-time pricing.
Note: This module is a work in progress. Features and APIs may change.
When you sell stocks, ETFs, or mutual funds in US markets, the proceeds settle two business days after the trade date (T+2). During this time:
- Your holdings will reflect the reduced share count immediately (as reported by your brokerage)
- Cash proceeds won't appear until settlement (typically 2 business days later)
- Total portfolio value may appear lower than expected until cash settles
Example: If you sell $20,000 worth of stock on Wednesday, your portfolio total will drop by ~$20,000 until Friday when the cash settles into your money market fund (SPAXX, etc.). This is normal brokerage behavior and not a bug.
If you need to manually account for pending settlements, you can add the expected cash to manual-holdings.json temporarily.
- Multi-Asset Support: Crypto, stocks, ETFs, mutual funds, and forex
- SnapTrade Integration: Fetches holdings from Fidelity, Coinbase, and other brokerages
- Coinbase Integration: Fetches crypto holdings via CDP API
- Twelve Data Integration: Stocks, ETFs, and forex pricing
- EODHD Integration: Mutual fund pricing with support for funds not covered by Twelve Data
- Manual Holdings: Support for manual entry without any API registrations
- Cost Basis & Gain/Loss: Display unrealized gain/loss percentages
- Portfolio Charts: Visual performance tracking over time
- Real-time Pricing: Configurable update intervals by asset type
- Market Hours Scheduling: Limit stock/forex polling to market hours
- 24h Change: Shows percent change for each holding
- Portfolio Total: Displays total value in configurable currency
- Forex Rates: Display exchange rates with optional inverse pairs
- Currency Conversion: Display values in any currency (USD, EUR, GBP, etc.)
- Privacy Mode: Hide quantity and total while keeping price and 24h change visible
- Secure Credentials: AES-256-GCM encrypted API keys
- Error Indicators: Visual warnings when sync fails
- Configurable Sorting: By value or alphabetically
MMM-Fintech supports multiple ways to track your holdings, from fully manual to fully automated.
Purpose: Track holdings without any API registrations or costs
Cost: Free
If you prefer not to register for any data provider APIs, you can manually enter your holdings in manual-holdings.json. This requires you to update the file yourself whenever your holdings change.
How it works: You enter your holdings (symbol, quantity, type) in a JSON file. The module will still need a pricing provider for market-priced holdings: Twelve Data for stocks/ETFs/forex, and EODHD for mutual funds when configured. You can also use the cash type for money market funds which uses a fixed $1.00 price with no API calls.
Best for: Users who want simplicity, don't want to share credentials with third-party services, or have holdings at brokerages not supported by SnapTrade.
See Manual Holdings Setup for the file format.
Purpose: Automatically fetches holdings from connected brokerage accounts (Fidelity, Coinbase, Schwab, etc.)
Cost: Pay-as-you-go pricing. See SnapTrade Pricing for details.
Why use it: SnapTrade provides unified access to multiple brokerages through a single API. When connected to Coinbase, it returns complete holdings including staked crypto (SOL, ETH) which the Coinbase CDP API cannot access. Holdings update automatically—no manual file editing required.
Get credentials:
- Create account at SnapTrade Dashboard
- Generate API key (clientId and consumerKey)
- See SnapTrade Getting Started for full setup
Purpose: Automatically fetches crypto holdings directly from Coinbase
Cost: Free. See Coinbase Developer Platform
Limitation: The CDP API does not return staked crypto assets. If you have staked SOL, ETH, or other assets, they will not appear. Use SnapTrade (connected to Coinbase) or manual holdings instead to track staked assets.
Get credentials:
- Go to CDP Portal API Keys
- Create a Secret API key with View permission
- Select ECDSA algorithm (required)
- Download the JSON file
Purpose: Provides real-time pricing for stocks, ETFs, and forex rates
Cost: Free tier includes 8 API calls/minute (800/day). See Twelve Data Pricing
Note: Twelve Data is a pricing provider only—it does not track holdings. Holdings come from SnapTrade, Coinbase, manual entry, or a combination. In the current provider split, Twelve Data is used for stocks, ETFs, and forex. With the default 20-minute update interval during market hours, you'll use approximately 200 calls/day for 10 symbols, well within the free tier.
Get credentials:
- Create account at Twelve Data
- Copy your API key from the dashboard
Purpose: Provides pricing for mutual funds, including funds that may not be available through Twelve Data
Cost: Free tier available, but small. See EODHD Pricing
Note: EODHD is optional and is only used for mutual_fund holdings when configured. MMM-Fintech resolves supported fund symbols internally to the appropriate exchange-form ticker such as .US or .NMFQS. Because the EODHD free tier is limited, mutual fund price refreshes are intentionally throttled more conservatively than stock/ETF updates.
Get credentials:
- Create account at EODHD
- Copy your API token from the dashboard
When fetching holdings, the module uses this priority:
- SnapTrade (if configured) — Fetches from all connected brokerages
- Coinbase CDP (if SnapTrade not configured) — Fetches crypto only
- Manual holdings — Always merged for additional positions or as standalone
For pricing:
- Crypto: Coinbase CDP API
- Stocks/ETFs/Forex: Twelve Data API
- Mutual Funds: EODHD when configured, otherwise Twelve Data fallback
- Cash (money market): Fixed at $1.00 (no API calls)
cd ~/MagicMirror/modules
git clone https://github.com/sonnyb9/MMM-Fintech.git
cd MMM-Fintech
npm installUse the unified setup wizard to configure all providers interactively:
cd ~/MagicMirror/modules/MMM-Fintech
node setup.jsThe wizard will guide you through:
- SnapTrade (automated brokerage holdings, includes staked crypto)
- Coinbase CDP API (crypto only, excludes staked assets)
- Twelve Data (stock/ETF/forex pricing)
- Manual Holdings (no API, manual JSON editing)
After configuring SnapTrade, run this to connect your brokerages:
node snaptrade-connect.jsOpen the URL in a browser to authorize access. Note: Connection URLs expire in 5 minutes.
If you prefer individual setup scripts instead of the wizard:
SnapTrade provides the most complete holdings data, including staked crypto.
node setup-snaptrade.jsEnter your clientId and consumerKey when prompted. Then generate a connection portal URL:
node snaptrade-connect.jsOpen the URL in a browser to connect your brokerage accounts (Fidelity, Coinbase, etc.). Note: The connection portal URL expires in 5 minutes—run the command again if it expires before you complete the connection.
Only needed if you're not using SnapTrade for crypto holdings.
- Download your API key JSON from CDP Portal
- Place
cdp_api_key.jsonin the module folder - Run:
node setup-credentials.jsThis will:
- Generate an encryption key at
~/.mmm-fintech-key - Create
cdp-credentials.encfrom your JSON file - Prompt to delete the original JSON (recommended)
node setup-twelvedata.jsEnter your API key when prompted. This creates twelvedata-credentials.enc.
node setup-eodhd.jsEnter your API token when prompted. This creates eodhd-credentials.enc.
Add to your config/config.js:
{
module: "MMM-Fintech",
position: "middle_center", // Recommended for best display
config: {
title: "Portfolio",
showLastUpdated: true,
showPricePerUnit: true,
showQuantity: true,
showForex: true,
sortBy: "value"
}
}Create manual-holdings.json in the module folder for any holdings not covered by APIs, or as your sole holdings source if not using SnapTrade or Coinbase:
{
"description": "Manual holdings and forex pairs",
"lastUpdated": "2025-12-29T00:00:00Z",
"holdings": [
{
"symbol": "SOL",
"quantity": 12.919,
"type": "crypto",
"source": "coinbase-staked",
"notes": "100% staked"
},
{
"symbol": "AAPL",
"quantity": 50,
"type": "stock",
"source": "fidelity"
},
{
"symbol": "VOO",
"quantity": 25,
"type": "etf",
"source": "fidelity"
},
{
"symbol": "SPAXX",
"quantity": 1000,
"type": "cash",
"source": "fidelity",
"notes": "Money market fund"
},
{
"symbol": "PRCFX",
"quantity": 53.498,
"type": "mutual_fund",
"source": "troweprice-ira",
"notes": "T. Rowe Price Capital Appreciation and Income Fund"
}
],
"forex": [
{"pair": "USD/PHP"},
{"pair": "USD/EUR"}
]
}Asset Types:
crypto- Cryptocurrency (priced via Coinbase)stock- Individual stocks (priced via Twelve Data)etf- Exchange-traded funds (priced via Twelve Data)mutual_fund- Mutual funds (priced via EODHD when configured, otherwise Twelve Data fallback)cash- Money market funds with stable $1.00 NAV (no API calls)
Optional Manual Fields:
costBasis- Total cost basis for gain/loss calculationsmanualPrice- Optional local override price per share/unit when the pricing provider does not support the symbolmanualChange24h- Optional 24h change percentage paired withmanualPrice(defaults to0)
Forex Pairs: Inverse rates shown as a column (can be hidden via showInverseForex: false)
Note: You must update this file manually whenever your holdings change. If using SnapTrade, this file is only needed for holdings at unsupported brokerages.
| Option | Default | Accepted Values | Description |
|---|---|---|---|
title |
"Portfolio" |
Any string | Header text |
fontSize |
100 |
Any number; interpreted as a percentage such as 80, 100, 120 |
Font size for the module wrapper |
displayMode |
"table" |
"table", "ticker" |
Display holdings as a table or a horizontally scrolling ticker |
tickerSpeed |
50 |
Any positive number | Ticker scroll speed in pixels per second |
tickerStartDelay |
3000 |
Any non-negative number | Milliseconds to hold the ticker still before scrolling starts |
tickerPause |
0 |
Any non-negative number | Milliseconds to pause on each item (0 disables pauses) |
tickerCollapseCash |
false |
true, false |
When true, combines cash-equivalent holdings into a single Cash ticker item |
currency |
"USD" |
Any currency code string such as "USD", "EUR", "GBP", "PHP" |
Currency code used for display; symbol rendering is built in for USD, EUR, GBP, JPY, CNY, PHP, CAD, AUD, CHF, INR, KRW, MXN, BRL, SGD, and HKD |
currencyStyle |
"symbol" |
"symbol", "code" |
Display values with a currency symbol or currency code |
cryptoPriceUpdateInterval |
300000 |
Any positive number | Crypto price refresh interval in milliseconds |
stockPriceUpdateInterval |
1200000 |
Any positive number | Stock, ETF, mutual fund, and forex refresh interval in milliseconds |
showLastUpdated |
true |
true, false |
Show the last successful sync timestamp |
showPricePerUnit |
true |
true, false |
Show the price-per-unit column |
showQuantity |
true |
true, false |
Show quantity and value columns, plus the total row |
showGainLoss |
true |
true, false |
Show gain/loss percentage when cost basis data is available |
showForex |
true |
true, false |
Show the forex rates section |
showInverseForex |
true |
true, false |
Show inverse rates in the forex section |
cryptoAsForex |
[] |
Array of crypto symbol strings, for example ["BTC", "ETH"] |
Treat selected crypto holdings as forex-style rates |
sortBy |
"value" |
"value", "name" |
Sort holdings by descending value or by symbol/name |
maxRetries |
6 |
Any non-negative integer | Maximum retry attempts for provider API requests |
holdingsSyncTime |
"07:45" |
Time string in HH:MM 24-hour format |
Daily holdings sync time |
staleHoldingsThreshold |
90000000 |
Any positive number | Holdings age threshold in milliseconds before data is marked stale |
stalePricesThreshold |
3900000 |
Any positive number | Price age threshold in milliseconds before data is marked stale |
showCharts |
false |
true, false |
Enable portfolio history charts |
chartMode |
"combined" |
"combined", "separate", "exclude-crypto" |
Use one combined chart, split traditional and crypto into separate charts, or hide crypto from the chart |
chartPeriod |
"1M" |
"1D", "1W", "1M", "3M", "1Y", "All" |
Default chart history period |
showPeriodSelector |
false |
true, false |
Show chart period buttons for touch or click interaction |
historyRetention |
1825 |
Any positive integer | Daily-history retention in days |
hourlyRetention |
720 |
Any positive integer | Hourly-history retention in hours |
marketHours |
See below | Object with stock, etf, mutual_fund, and forex schedules |
Market-hour polling schedules by asset type |
Stock, ETF, and mutual fund price updates are limited to US market hours by default. Forex updates follow the 24/5 forex market schedule (Sunday 5pm - Friday 5pm ET). This reduces unnecessary API calls and rate limit issues outside trading hours.
Default Configuration:
marketHours: {
stock: {
enabled: true,
timezone: "America/New_York",
open: "09:30",
close: "16:00",
days: [1, 2, 3, 4, 5], // Mon-Fri (0=Sun, 6=Sat)
postClosePoll: true // One final update after close
},
etf: {
enabled: true,
timezone: "America/New_York",
open: "09:30",
close: "16:00",
days: [1, 2, 3, 4, 5],
postClosePoll: true
},
mutual_fund: {
enabled: true,
timezone: "America/New_York",
open: "09:30",
close: "16:00",
days: [1, 2, 3, 4, 5],
postClosePoll: true
},
forex: {
enabled: true,
timezone: "America/New_York",
sundayOpen: "17:00", // Market opens Sunday 5pm ET
fridayClose: "17:00" // Market closes Friday 5pm ET
}
}Disable Market Hours (poll 24/7):
config: {
marketHours: {
stock: { enabled: false },
etf: { enabled: false },
mutual_fund: { enabled: false },
forex: { enabled: false }
}
}Key behaviors:
- postClosePoll: When enabled, allows one final price update after market close each trading day
- days: Array of trading days (0=Sunday through 6=Saturday)
- Crypto: Always updates regardless of market hours (24/7 market)
config: {
showQuantity: false,
showPricePerUnit: true
}config: {
currency: "EUR",
currencyStyle: "symbol"
}config: {
cryptoAsForex: ["BTC", "ETH"],
showInverseForex: false
}config: {
fontSize: 80
}config: {
showCharts: true,
chartPeriod: "1M"
}config: {
showCharts: true,
chartMode: "separate"
}Ticker mode displays holdings as a horizontal scrolling ticker bar, ideal for TV displays or landscape orientations.
Features:
- Portfolio total shown first with value-weighted 24h change
- Cash-equivalent holdings appear immediately after the portfolio total
- Cash-equivalent holdings show total value instead of a static
$1.00NAV - Each holding shows: Symbol, Price, Change %
- Green/red color coding for positive/negative changes
(Closed)indicator for non-crypto assets when markets are closed- Charts can still be displayed alongside the ticker
Basic Ticker Configuration:
config: {
displayMode: "ticker",
tickerSpeed: 50, // pixels per second
tickerStartDelay: 3000,
tickerPause: 0 // no pause between items
}Ticker with Collapsed Cash:
config: {
displayMode: "ticker",
tickerSpeed: 50,
tickerStartDelay: 3000,
tickerCollapseCash: true
}Ticker with Chart:
config: {
displayMode: "ticker",
tickerSpeed: 40,
tickerStartDelay: 3000,
showCharts: true,
chartMode: "combined",
chartPeriod: "1W"
}Market Status:
- Crypto: Always shows live 24h change (24/7 market)
- Stocks/ETFs/Mutual Funds: Shows change since previous close during market hours; shows last trading day's change with
(Closed)indicator when markets are closed - Forex: Shows change since previous close during trading hours (Sun 5pm - Fri 5pm ET)
The module can display portfolio value charts over time.
Chart Modes:
combined- Single chart showing total portfolio valueseparate- Two charts: Traditional investments + Cryptoexclude-crypto- Single chart showing only traditional investments
Chart Defaults:
chartMode: "combined"chartPeriod: "1M"showPeriodSelector: false
Time Periods:
1D- Last 24 hours (uses hourly snapshots)1W- Last 7 days (uses hourly snapshots for higher granularity)1M- Last 30 days (uses hourly snapshots if available, falls back to daily)3M,1Y,All- Use daily snapshots
Chart Features:
- Cost Basis Line: Dashed horizontal line showing your total cost basis (requires cost basis data from SnapTrade or manual holdings)
- Period Change Label: Shows the percentage change over the displayed period (e.g., "+5.23%") in green/red
- Optional Period Buttons: Set
showPeriodSelector: trueto show1D,1W,1M,3M,1Y, andAllbuttons in the UI - Improved Tick Marks: Cleaner Y-axis formatting with appropriate precision
Data Collection:
- Hourly snapshots recorded during each price update (default: 30 days / 720 hours retention)
- Daily snapshots recorded during the morning holdings sync (default: 5 years retention)
- Data starts fresh from when you enable charts (no historical backfill)
Storage Requirements (10 holdings):
- ~120 bytes per hourly snapshot
- ~400 bytes per daily snapshot
- ~100 KB for 30 days of hourly data
- ~150 KB per year of daily data
- Holdings sync: Daily at configured time (default: 7:45am), plus on startup if data >24 hours old
- Crypto prices: Every 5 minutes (configurable), 24/7
- Stock/ETF prices: Every 20 minutes during market hours (configurable)
- Forex prices: Every 20 minutes during forex market hours (Sun 5pm - Fri 5pm ET)
Twelve Data Free Tier: 800 calls/day
- With 20-minute intervals during market hours: ~200 calls/day for 10 symbols
- Currency conversion adds 1 extra call per sync (if not USD)
- Crypto uses Coinbase (separate limit)
| File | Description | Git |
|---|---|---|
MMM-Fintech.js |
Frontend: DOM rendering, socket handling | Tracked |
node_helper.js |
Backend: Provider coordination, scheduling, caching | Tracked |
MMM-Fintech.css |
Module styling | Tracked |
providers/index.js |
Factory functions for provider creation | Tracked |
providers/base.js |
Base class with shared utilities | Tracked |
providers/coinbase.js |
Coinbase CDP API provider (crypto) | Tracked |
providers/eodhd.js |
EODHD provider (mutual fund pricing) | Tracked |
providers/twelvedata.js |
Twelve Data API provider (stocks/ETFs/forex) | Tracked |
providers/snaptrade.js |
SnapTrade API provider (brokerage holdings) | Tracked |
lib/history-manager.js |
Chart history snapshot management | Tracked |
health-check.js |
Runs the bundled diagnostics with a summary report | Tracked |
setup-credentials.js |
CLI tool to encrypt Coinbase CDP API key | Tracked |
setup-eodhd.js |
CLI tool to encrypt EODHD API key | Tracked |
setup-twelvedata.js |
CLI tool to encrypt Twelve Data API key | Tracked |
setup-snaptrade.js |
CLI tool to encrypt SnapTrade credentials | Tracked |
snaptrade-connect.js |
CLI tool to generate brokerage connection URL | Tracked |
test-eodhd.js |
CLI diagnostic for EODHD mutual fund pricing | Tracked |
cdp_api_key.json |
Original Coinbase key (delete after setup) | Ignored |
cdp-credentials.enc |
Encrypted Coinbase credentials | Ignored |
eodhd-credentials.enc |
Encrypted EODHD credentials | Ignored |
twelvedata-credentials.enc |
Encrypted Twelve Data credentials | Ignored |
snaptrade-credentials.enc |
Encrypted SnapTrade credentials | Ignored |
~/.mmm-fintech-key |
Encryption key (shared by all providers) | N/A |
manual-holdings.json |
Manual holdings and forex | Ignored |
cache.json |
Cached data | Ignored |
history.json |
Chart history snapshots | Ignored |
Crypto (Coinbase):
- Use uppercase:
BTC,ETH,SOL - Check: Coinbase Advanced Trade
Stocks/ETFs (Twelve Data):
- Use standard tickers:
AAPL,GOOGL,VOO,SPY - Check: Twelve Data Symbol Search
Mutual Funds (EODHD when configured, otherwise Twelve Data fallback):
- Use standard fund tickers:
PRCFX,PREIX,FXAIX - EODHD resolves supported symbols internally to exchange-form tickers like
.USor.NMFQS
Forex (Twelve Data):
- Format:
BASE/QUOTE(e.g.,USD/PHP,EUR/USD)
pm2 logs <your-magicmirror-pm2-process> --lines 100 | grep -i fintechrm ~/MagicMirror/modules/MMM-Fintech/cache.json
pm2 restart <your-magicmirror-pm2-process>The module includes test scripts to verify provider connections and data flow:
cd ~/MagicMirror/modules/MMM-FintechQuick health check: Run all diagnostic tests in sequence with a summary report:
node health-check.jsThis runs all five test scripts below and provides a pass/fail summary with timing information. Use this as your first diagnostic step.
| Script | Purpose | When to Use |
|---|---|---|
test-snaptrade-provider.js |
Tests SnapTrade initialization and holdings fetch | After SnapTrade setup to verify credentials and brokerage connections |
test-twelvedata.js |
Tests Twelve Data pricing for stocks, ETFs, and forex | After Twelve Data setup to verify API key and symbol availability |
test-eodhd.js |
Tests EODHD pricing for mutual funds | After EODHD setup or when a mutual fund is not pricing correctly |
test-full-sync.js |
Tests complete holdings sync across configured providers | When holdings aren't appearing or to debug provider priority |
test-costbasis.js |
Tests cost basis and gain/loss data from SnapTrade | When G/L column shows incorrect values or dashes |
Example usage:
# Run complete health check (recommended)
node health-check.js
# Or run individual tests for specific diagnostics
node test-snaptrade-provider.js
node test-twelvedata.js
node test-eodhd.js
node test-full-sync.js"Crypto holdings data is N hours old"
- Check sync logs:
pm2 logs <your-magicmirror-pm2-process> --lines 100 | grep -E "Holdings sync" - Restart:
pm2 restart <your-magicmirror-pm2-process>
"Invalid symbol 'XYZ'"
- Verify symbol exists on the appropriate exchange
- Check
typefield matches the asset (crypto vs stock)
"TwelveData provider not configured"
- Run
node setup-twelvedata.jsto add credentials
"EODHD provider not configured"
- Optional for mutual funds
- Run
node setup-eodhd.jsto add credentials
"SnapTrade provider not configured"
- Run
node setup-snaptrade.jsto add credentials
"Rate limit exceeded"
- Twelve Data: Wait for credit reset (per minute)
- Coinbase: Uses exponential backoff automatically
"Market closed for stock, skipped price refresh"
- Normal behavior outside market hours
- Set
marketHours.stock.enabled: falseto disable
"SnapTrade API timeout - check network/service status"
- SnapTrade API failed to respond within timeout period (30 seconds)
- Module automatically retries with exponential backoff: 1min, 2min, 4min
- After 4 total attempts (1 initial + 3 retries), error is displayed in footer
- Check logs for detailed retry information:
pm2 logs <your-magicmirror-pm2-process> | grep SnapTrade - Usually resolves automatically on next sync cycle
- If persistent, check SnapTrade service status or network connectivity
The SnapTrade provider includes robust retry logic with exponential backoff:
- Timeout: 30 seconds per API call
- Retry delays: 1 minute → 2 minutes → 4 minutes
- Total attempts: 4 (initial + 3 retries)
- Logging: All attempts and failures are logged with timestamps
If all retries are exhausted, the module continues operation with stale data and displays a timeout warning in the footer. The error clears automatically on the next successful sync.
See ROADMAP.md for planned features and development phases.
MIT