PortfolioSyncing
PortfolioSyncing
Safely import broker CSV position exports into the Google Sheets DataHub tab, ensuring data integrity, validating changes, and protecting sacred formulas.
Multi-Broker Support
Supported Brokers:
- ✅ Fidelity - Fully automated parsing
- ⚠️ Schwab, Vanguard, TD Ameritrade, E*TRADE, Robinhood - Manual mapping required (coming soon)
Broker Detection: Finance Guru automatically detects your broker from user-profile.yaml (set during onboarding). CSV parsing is tailored to your broker's format.
See: docs/broker-csv-export-guide.md for detailed export instructions per broker.
Workflow Routing
When executing this workflow, output this notification:
Running the **SyncPortfolio** workflow from the **PortfolioSyncing** skill...
| Workflow | Trigger | File |
|---|---|---|
| SyncPortfolio | "sync portfolio", "portfolio-sync", "import fidelity" | workflows/SyncPortfolio.md |
Examples
Example 1: Sync after downloading new Fidelity CSV
User: "portfolio-sync"
-> Reads Portfolio_Positions_*.csv and Balances_*.csv from notebooks/updates/
-> Compares with Google Sheets DataHub
-> Updates quantities, cost basis, SPAXX, margin debt
-> Reports changes and validates formulas
Example 2: Update positions after trades
User: "I just bought more JEPI, sync my portfolio"
-> Invokes SyncPortfolio workflow
-> Detects quantity change in JEPI
-> If >10% change, asks for confirmation
-> Updates DataHub with new position data
Example 3: Import new Fidelity export
User: "import fidelity CSV"
-> Locates latest CSV files by date
-> Runs safety checks (position count, large changes)
-> Syncs all positions and cash/margin values
-> Logs update summary
Core Workflow
1. Read Latest Fidelity CSVs
Positions File: notebooks/updates/Portfolio_Positions_MMM-DD-YYYY.csv
Key Fields to Extract:
- Symbol → Column A: Ticker
- Quantity → Column B: Quantity
- Average Cost Basis → Column G: Avg Cost Basis
CSV Format:
Symbol,Quantity,Last Price,Current Value,Total Gain/Loss Dollar,...,Average Cost Basis
TSLA,74,$445.47,$32964.78,+$15634.71,...,$234.19
PLTR,369.746,$188.90,$69845.01,+$60235.59,...,$25.99
Balances File: notebooks/updates/Balances_for_Account_{account_id}.csv
Key Fields to Extract for Cash & Margin:
- "Settled cash" → Use for SPAXX row (Column L: Current Value)
- "Account equity percentage" → If 100%, margin debt = $0
- "Net debit" → Actual margin balance (negative value = margin debt)
- "Margin interest accrued this month" → If > $1, there IS margin debt
⚠️ IMPORTANT: Cash Position Logic
- Do NOT use
SPAXXvalue from Positions CSV (shows only settled money market) - Use "Settled cash" from Balances CSV for the SPAXX row
- If "Settled cash" = 0, then SPAXX = $0 (all funds are invested or in margin)
- "Cash market value" is NOT cash - it's the value of positions in your Cash account (vs Margin account)
Margin Debt Logic:
IF "Account equity percentage" == 100% THEN
Margin Debt = $0.00
ELSE
Margin Debt = Total Account Value × (1 - Equity Percentage)
END
2. Compare with Current Sheet
Read from Google Sheets DataHub:
- Column A: Ticker
- Column B: Quantity
- Column G: Avg Cost Basis
Identify:
- ✅ NEW tickers: In CSV but not in sheet (additions)
- ✅ EXISTING tickers: In both (updates)
- ⚠️ MISSING tickers: In sheet but not in CSV (possible sales)
3. Safety Checks (STOP if triggered)
Position Mismatches:
- If CSV has fewer tickers than sheet, STOP and alert user
- User must confirm sales/transfers before proceeding
Large Quantity Changes (>10%):
- If any ticker quantity changes more than 10%, STOP and show diff
- Example: TSLA goes from 74 → 85 shares (+14.9%) = ALERT
- User must confirm intentional trades
Cost Basis Changes (>20%):
- If average cost basis changes more than 20%, FLAG for review
- Possible corporate action (split, merger, dividend reinvestment)
- User should verify this is correct
Formula Validation:
- Scan Columns C-S for #N/A, #DIV/0!, #REF! errors before updating
- If 3+ errors detected, STOP and suggest formula repair first
3.5 Transaction History Cross-Check (Optional Validation)
Transactions File: notebooks/transactions/History_for_Account_{account_id}.csv
When large quantity changes (>10%) are detected, cross-reference with the transaction history to validate:
Key Fields to Check:
- Run Date → Date of transaction
- Action → BUY, SELL, DIVIDEND, etc.
- Symbol → Ticker symbol
- Quantity → Shares bought/sold
- Amount → Dollar value of transaction
Validation Logic:
For each ticker with >10% change:
1. Read transaction history for that ticker
2. Sum recent BUY transactions since last sync
3. Verify: Current CSV Qty ≈ Previous Sheet Qty + Net Transactions
4. If mismatch > 1 share, FLAG for manual review
Example Cross-Check:
JEPI shows +18.9 shares (90.82 → 109.72)
Transaction History shows:
- Dec 15: BUY JEPI 10 shares
- Dec 18: BUY JEPI 8.9 shares (DRIP)
Total: +18.9 shares ✅ VERIFIED
When to Skip:
- Small changes (<10%) - trust CSV
- User explicitly confirms changes
- Transaction file unavailable or outdated
4. Update Operations
For EXISTING Tickers:
Update Column B (Quantity) = CSV Quantity
Update Column G (Avg Cost Basis) = CSV Average Cost Basis
DO NOT TOUCH:
- Column C (Last Price) - Google Finance formula auto-updates
- Columns D-F ($ Change, % Change, Volume) - Formulas/Alpha Vantage
- Columns H-M (Gains/Losses) - Calculated formulas
- Columns N-S (Ranges, dividends, layer) - Formulas or manual classification
🚨 CRITICAL: NEVER PASS EMPTY STRINGS TO FORMULA COLUMNS
- WRONG: Updating entire row range with empty strings (
"") will OVERWRITE formulas - RIGHT: Update ONLY columns A, B, G using individual cell ranges
- Empty strings (
"") will DELETE formulas in columns C-F - this BREAKS the sheet
For NEW Tickers:
1. Add new row
2. Set Column A (Ticker) = CSV Symbol
3. Set Column B (Quantity) = CSV Quantity
4. Set Column G (Avg Cost Basis) = CSV Average Cost Basis
5. Apply pattern-based layer classification to Column S:
- If ticker in [JEPI, JEPQ, SPYI, QQQI, CLM, CRF, etc.] → "Layer 2 - Dividend"
- If ticker in [SQQQ] → "Layer 3 - Hedge"
- If ticker in [TSLA, PLTR, NVDA, COIN, MSTR, SOFI] → "Layer 1 - Growth"
- If ticker in [VOO, VTI, FZROX, FNILX] → "Layer 1 - Index"
6. Column C (Last Price) will auto-populate from GOOGLEFINANCE formula
Log Addition:
Added {TICKER} - {SHARES} shares @ ${AVG_COST} - Layer: {LAYER}
Example: Added MSTY - 87.9 shares @ $11.94 - Layer: Layer 2 - Dividend
5. Update Cash & Margin Rows (MANDATORY)
🚨 CRITICAL: This step is NOT optional. SPAXX and Margin must be updated every sync.
SPAXX (Cash Position) - Row 37, Column L:
1. Read "Settled cash" from Balances CSV
2. If "Settled cash" = 0 → Update DataHub!L37 with " $ - " (zero cash)
3. If "Settled cash" > 0 → Update DataHub!L37 with formatted value
4. ⚠️ SAFETY CHECK: If current sheet SPAXX differs from CSV by >$100, FLAG for user
Pending Activity - Row 38, Column L:
1. Read "Net debit" from Balances CSV (will be negative if margin used)
2. Update DataHub!L38 with this value (format: " $ (X,XXX.XX)" for negative)
Margin Debt - Row 39, Column L:
1. Read "Net debit" from Balances CSV
2. Convert to positive: Margin Debt = ABS(Net debit)
3. Update DataHub!L39 with positive value (format: " $ X,XXX.XX ")
4. If Net debit = 0 → Update with " $ - "
Example:
// Cash position from Balances CSV "Settled cash" = 0
mcp__gdrive__sheets(operation: "updateCells", params: {
spreadsheetId: SPREADSHEET_ID,
range: "DataHub!L37:L37",
values: [[" $ - "]]
})
// Pending Activity from "Net debit" = -7822.71
mcp__gdrive__sheets(operation: "updateCells", params: {
spreadsheetId: SPREADSHEET_ID,
range: "DataHub!L38:L38",
values: [[" $ (7,822.71)"]]
})
// Margin debt = ABS(-7822.71) = 7822.71
mcp__gdrive__sheets(operation: "updateCells", params: {
spreadsheetId: SPREADSHEET_ID,
range: "DataHub!L39:L39",
values: [[" $ 7,822.71 "]]
})
6. Post-Update Validation
Verify:
- Google Finance formulas auto-populated prices for new tickers
- Formulas still functional (no new #N/A errors)
- Row count matches expected additions
- Total account value approximately matches Fidelity total
- SPAXX reflects "Settled cash" from Balances CSV (MANDATORY)
- Pending Activity reflects "Net debit" from Balances CSV (MANDATORY)
- Margin Debt = ABS(Net debit) (MANDATORY)
Log Update Summary:
✅ Updated 25 positions (quantity + cost basis)
✅ Added 3 new tickers: MSTY, YMAX, AMZY
✅ SPAXX updated: $0 (Settled cash = 0)
✅ Pending Activity: -$7,822.71 (Net debit)
✅ Margin debt: $7,822.71 (ABS of Net debit)
✅ No formula errors detected
✅ Portfolio value: $228,809.41 (matches Fidelity)
Critical Rules
WRITABLE Columns (from CSV)
- ✅ Column A: Ticker
- ✅ Column B: Quantity
- ✅ Column G: Avg Cost Basis
SACRED Columns (NEVER TOUCH)
- ❌ Column C: Last Price (GOOGLEFINANCE formulas)
- ❌ Columns D-F: $ Change, % Change, Volume (formulas)
- ❌ Columns H-M: Gains/Losses calculations (formulas)
- ❌ Columns N-S: Ranges, dividends, layer (formulas/manual)
Pattern-Based Layer Classification
Use these patterns to auto-classify new tickers in Column S:
Layer 2 - Dividend (Income funds):
- JEPI, JEPQ, SPYI, QQQI, QQQY
- CLM, CRF, ETY, ETV, BDJ, UTG, BST
- MSTY, YMAX, AMZY
- Any ticker with "yield" or "income" in description
Layer 3 - Hedge (Downside protection):
- SQQQ (ProShares UltraPro Short QQQ)
Layer 1 - Growth (Core holdings):
- TSLA, PLTR, NVDA, AAPL, GOOGL
- COIN, MSTR (Bitcoin proxies)
- SOFI
Layer 1 - Index (Passive core):
- VOO, VTI, VUG, QQQ
- FZROX, FNILX, FZILX, VXUS
Safety Gates
STOP conditions (require user confirmation):
- CSV has fewer tickers than sheet (possible sales)
- Any quantity change > 10%
- Any cost basis change > 20%
- 3+ formula errors detected
- Margin balance jumped > $5,000 (unintentional draw)
- SPAXX discrepancy > $100 (cash mismatch between sheet and CSV)
FLAG conditions (alert user but proceed):
- SPAXX differs from "Settled cash" by $1-$100 (minor discrepancy)
- Pending Activity differs from "Net debit" by >$100
When STOPPED:
- Show clear diff table
- Ask user to confirm changes
- Proceed only after explicit approval
When FLAGGED:
- Show the discrepancy
- Proceed with update but highlight in summary
Example Scenario
User downloads: Portfolio_Positions_Nov-11-2025.csv
Agent workflow:
- ✅ Read CSV - found 35 positions
- ✅ Compare with sheet - 32 existing positions
- ⚠️ NEW TICKERS DETECTED:
- MSTY: 87.9 shares @ $11.94
- YMAX: 110.982 shares @ $12.32
- AMZY: 65.748 shares @ $14.44
- ✅ SAFETY CHECKS PASSED - No large changes
- ✅ UPDATE OPERATIONS:
- Updated 32 existing positions (B, G columns)
- Added 3 new tickers with Layer 2 classification
- ✅ VALIDATION - All formulas working, no errors
- ✅ LOG: "Updated 32 positions, added 3 new dividend funds"
Google Sheets Integration
Spreadsheet ID: Read from fin-guru/data/user-profile.yaml → google_sheets.portfolio_tracker.spreadsheet_id
❌ BAD: Multi-Column Range Updates with Empty Strings
THIS WILL BREAK FORMULAS:
// ❌ WRONG - Passing empty strings overwrites formulas in columns C-F
mcp__gdrive__sheets(
operation: "updateCells",
params: {
spreadsheetId: SPREADSHEET_ID,
range: "DataHub!A13:G27", // ❌ Multi-column range
values: [
["JEPI", "72.942", "", "", "", "", "$56.48"], // ❌ Empty strings kill formulas
["JEPQ", "92.043", "", "", "", "", "$58.08"],
["CLM", "763.367", "", "", "", "", "$8.32"]
]
}
)
Why this breaks: Empty strings ("") in columns C-F DELETE the GOOGLEFINANCE and calculation formulas.
✅ GOOD: Individual Cell Updates (Columns A, B, G Only)
THIS PRESERVES FORMULAS:
// ✅ RIGHT - Update ONLY writable columns, one at a time
// Update JEPI quantity (Column B only)
mcp__gdrive__sheets(
operation: "updateCells",
params: {
spreadsheetId: SPREADSHEET_ID,
range: "DataHub!B13:B13", // ✅ Single column, specific row
values: [["72.942"]]
}
)
// Update JEPI cost basis (Column G only)
mcp__gdrive__sheets(
operation: "updateCells",
params: {
spreadsheetId: SPREADSHEET_ID,
range: "DataHub!G13:G13", // ✅ Single column, specific row
values: [["$56.48"]]
}
)
// Add new ticker (Columns A, B, G - formulas in C-F will auto-populate)
mcp__gdrive__sheets(
operation: "updateCells",
params: {
spreadsheetId: SPREADSHEET_ID,
range: "DataHub!A28:A28", // ✅ Ticker only
values: [["ECAT"]]
}
)
mcp__gdrive__sheets(
operation: "updateCells",
params: {
spreadsheetId: SPREADSHEET_ID,
range: "DataHub!B28:B28", // ✅ Quantity only
values: [["72.884"]]
}
)
mcp__gdrive__sheets(
operation: "updateCells",
params: {
spreadsheetId: SPREADSHEET_ID,
range: "DataHub!G28:G28", // ✅ Cost basis only
values: [["$15.92"]]
}
)
Why this works: Only touching columns A, B, G leaves formulas in C-F intact and functional.
Update Pattern Summary
| Action | ✅ GOOD | ❌ BAD |
|---|---|---|
| Update quantity | range: "DataHub!B13:B13" |
range: "DataHub!A13:G13" with ["", "72.942", "", "", "", "", ""] |
| Update cost basis | range: "DataHub!G13:G13" |
Including columns C-F in range |
| Add new ticker | 3 separate calls (A, B, G) | Single call with empty strings in C-F |
| Multiple tickers | Loop through rows, update B and G individually | Batch update entire range A:G |
Golden Rule: NEVER include columns C-F in your update range. NEVER pass empty strings to any cell.
Agent Permissions
Builder (Write-enabled):
- Can update columns A, B, G
- Can add new rows
- Can apply layer classification
- CANNOT modify formulas
All Other Agents (Read-only):
- Market Researcher, Quant Analyst, Strategy Advisor
- Can read all data
- Cannot write to spreadsheet
- Must defer to Builder for updates
Reference Files
For complete architecture details, see:
- Full Architecture:
fin-guru/data/spreadsheet-architecture.md - Quick Reference:
fin-guru/data/spreadsheet-quick-ref.md - User Profile:
fin-guru/data/user-profile.yaml
Pre-Flight Checklist
Before importing CSV:
- Positions CSV (
Portfolio_Positions_*.csv) is latest by date - Balances CSV (
Balances_for_Account_*.csv) is available and current - Both CSVs are from Fidelity (not M1 Finance or other broker)
- Files are in
notebooks/updates/directory - Google Sheets DataHub tab exists
- No pending manual edits in sheet (user should save first)
- Current portfolio value is known (for validation)
⚠️ BOTH CSVs Required: Positions CSV alone is insufficient. Balances CSV provides:
- "Settled cash" → SPAXX value
- "Net debit" → Pending Activity and Margin Debt values
Skill Type: Domain (workflow guidance) Enforcement: BLOCK (data integrity critical) Priority: Critical Line Count: < 300 (following 500-line rule) ✅