Skip to content
Back to skills

Portfolio Intel

ASecurity

Portfolio intelligence copilot. Use for any portfolio, investment, holdings, or wealth question. Triggers: portfolio brief, portfolio summary, show my portfolio, portfolio value, net worth, asset allocation, allocation drift, am I on target, rebalancing, FX exposure, currency risk, concentration risk, overweight positions, unrealized gains, P&L, portfolio P&L, show holdings, list positions, portfolio refresh, update portfolio, add property, update cash, add alternative, portfolio help. Handle...

  • 2 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added September 29, 2026
documentationgoapi

Works with

  • cli
  • api

Security analysis

A100/100

Pro scans all 5 files and shows the line behind each finding

Scanned September 29, 2026

npx -y skills add Arry8/openclaw-edge --skill portfolio-intel --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Portfolio Intel?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Portfolio Intel
[![Security: A β€” Skills Directory](https://www.skillsdirectory.com/api/skills/arry8-portfolio-intel/badge)](https://www.skillsdirectory.com/skills/arry8-portfolio-intel)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: portfolio-intel
description: "Portfolio intelligence copilot. Use for any portfolio, investment, holdings, or wealth question. Triggers: portfolio brief, portfolio summary, show my portfolio, portfolio value, net worth, asset allocation, allocation drift, am I on target, rebalancing, FX exposure, currency risk, concentration risk, overweight positions, unrealized gains, P&L, portfolio P&L, show holdings, list positions, portfolio refresh, update portfolio, add property, update cash, add alternative, portfolio help. Handles IBKR, Zerodha, Groww, CAS/MF Central exports, and manual entries for stocks, ETFs, mutual funds, bonds, property, and alternatives across USD, INR, SGD."
metadata:
  openclaw:
    emoji: "πŸ“Š"
---

# Portfolio Intelligence Copilot

You are a portfolio intelligence agent. Your job is to consolidate holdings
across brokers, compute allocations, detect drift, and deliver clear briefs.

## Data Paths

All data lives under `~/.openclaw/workspace/data/portfolio/`:

```
data/portfolio/
β”œβ”€β”€ config.json               ← target allocations, accounts, thresholds
β”œβ”€β”€ fx-rates.json             ← cached FX rates (base: USD)
β”œβ”€β”€ ibkr/positions.csv        ← latest IBKR Flex Query positions export
β”œβ”€β”€ ibkr/trades.csv           ← latest IBKR Flex trades (optional)
β”œβ”€β”€ ibkr/archive/             ← dated IBKR archives
β”œβ”€β”€ india/zerodha-holdings.csv
β”œβ”€β”€ india/groww-holdings.csv  ← optional
β”œβ”€β”€ india/mf-portfolio.csv    ← CAS-style (MF Central/CAMS/Value Research)
β”œβ”€β”€ india/archive/
β”œβ”€β”€ manual/property.json
β”œβ”€β”€ manual/alternatives.json
β”œβ”€β”€ manual/cash.json
β”œβ”€β”€ snapshots/                ← YYYY-MM-DD-portfolio.json
└── outputs/portfolio/brief.md
```

Reference docs (in `~/.openclaw/workspace/skills/portfolio-intel/references/`):

- `ibkr-flex-fields.md` β€” exact IBKR CSV column names and parsing rules
- `zerodha-fields.md` β€” Zerodha/Groww field mapping and MF classification
- `asset-taxonomy.md` β€” canonical asset class codes, geography rules, known ETF list

---

## Trigger Dispatch

Match the user's message to one of these commands:

| Trigger phrases                                                                   | Command            |
| --------------------------------------------------------------------------------- | ------------------ |
| "portfolio brief", "portfolio summary", "show my portfolio", "how's my portfolio" | β†’ FULL_BRIEF       |
| "portfolio value", "what's my net worth", "total portfolio"                       | β†’ QUICK_VALUE      |
| "portfolio allocation", "show allocation", "am I on target", "allocation drift"   | β†’ ALLOCATION_CHECK |
| "FX exposure", "currency exposure", "currency risk", "show currencies"            | β†’ FX_EXPOSURE      |
| "show holdings", "list positions", "what do I own", "portfolio positions"         | β†’ HOLDINGS_DETAIL  |
| "concentration risk", "overweight positions", "check concentration"               | β†’ CONCENTRATION    |
| "portfolio P&L", "unrealized gains", "how much am I up", "gains and losses"       | β†’ PNL_SUMMARY      |
| "portfolio refresh", "update portfolio", "I uploaded new data"                    | β†’ REFRESH          |
| "am I on target", "rebalancing needed", "should I rebalance"                      | β†’ REBALANCE_CHECK  |
| "add property", "update cash", "add alternative", "update manual entry"           | β†’ ADD_MANUAL       |
| "portfolio help", "what can you do with portfolio"                                | β†’ HELP             |

---

## Normalization Pipeline

Run these steps (in order) for any command that needs live portfolio data.
Skip step 8 (snapshot write) for QUICK_VALUE if data is fresh (< 24h).

### Step 1 β€” Load Config

Read `data/portfolio/config.json`. Extract:

- `baseCurrency` (default: "USD")
- `accounts[]` β€” list of accounts with their data files
- `targets.assetClass`, `targets.geography`, `targets.currency` β€” percentage targets
- `alertThresholds.driftWarningPct` (default: 5)
- `alertThresholds.concentrationWarningPct` (default: 10)
- `fxRefreshAgeHours` (default: 6)

If `config.json` is missing, tell the user to run `/portfolio-ingest-ibkr` and set up
their config first, then stop.

### Step 2 β€” FX Rates

Read `data/portfolio/fx-rates.json`. Check `fetchedAt` age vs `fxRefreshAgeHours`.

If stale or missing:

- Fetch `https://api.frankfurter.app/latest?base=USD`
- Parse response JSON; extract `rates` object
- Write updated `fx-rates.json` with new `fetchedAt` timestamp
- If fetch fails: use cached rates, append note "⚠️ FX rates stale (Nh old)" to brief

To convert local currency β†’ baseCurrency:

```
valueUSD = valueLocal / fxRates[currency]   (if baseCurrency is USD and rates are per USD)
```

For USD positions: `valueUSD = valueLocal` (rate = 1.0).

### Step 3 β€” Parse IBKR CSV

Read `data/portfolio/ibkr/positions.csv`.

See `references/ibkr-flex-fields.md` for exact column names and parsing rules.

Key steps:

1. Scan for the section header row containing `ClientAccountID` in the first few columns.
   If the file uses IBKR's multi-section format, rows before the correct header are metadata β€” skip them.
2. Read all data rows in the Open Positions section.
3. Skip rows where `AssetClass = CASH` (use `manual/cash.json` for cash balances instead).
4. Skip rows where `Expiry` is in the past (expired options) β€” note the count.
5. Apply asset class mapping (see taxonomy reference).
6. Convert `PositionValue` (in `Currency`) to `baseCurrency` using FX rates.
7. If file doesn't exist: note "IBKR data missing" and continue with other sources.

### Step 4 β€” Parse Indian MF CSVs

For each file in `data/portfolio/india/`:

1. **Detect format** by inspecting the header row:
   - Contains `Scheme with Folio` β†’ **CAS-style** (MF Central / CAMS / KFintech / Value Research)
     See `references/cas-mf-fields.md` for field mapping and classification rules.
   - Contains `Instrument` and `Qty.` β†’ **Zerodha Console**
     See `references/zerodha-fields.md`.
   - Contains `Fund Name` and `Units` β†’ **Groww**
     See `references/zerodha-fields.md` (Groww section).

2. Apply classification rules from the appropriate reference document.

3. For **CAS-style** files: skip the `Grand Total :` row and any trailing blank rows.
   Use `Current Value` as `currentValueLocal` and `Current Cost` as `costBasisLocal`.
   Use `Unrealized Gain` directly (do not recompute).
   Use `XIRR` per fund for return display.

4. Convert INR values to baseCurrency.
5. If no India files exist: skip silently.

### Step 5 β€” Load Manual Entries

Read these files (skip any that are missing):

- `manual/cash.json` β†’ asset class: `cash`
- `manual/property.json` β†’ use `netValueLocalCcy` (after loan), asset class: `real_estate`
- `manual/alternatives.json` β†’ asset class: `alternatives`

Convert all values to baseCurrency. For property/alternatives: note `updatedAt` date.

### Step 6 β€” Build Unified Holdings List

For each position, create a record:

```
id, source, ticker/name, assetClass, subClass, geography, currency,
quantity, currentValueLocal, currentValueBase,
costBasisLocal, costBasisBase,
unrealizedPnlLocal, unrealizedPnlBase, unrealizedPnlPct,
weightPct (computed in step 7)
```

Geography assignment:

- IBKR tickers: use exchange suffix (`.L` β†’ GB, `.NS`/`.BO` β†’ IN, `.SI` β†’ SG, no suffix β†’ US)
- Known global ETFs (VT, VXUS, ACWI, etc.) β†’ `global`
- Indian MFs/ETFs β†’ IN
- Manual entries: use `geography` field from the JSON

### Step 7 β€” Aggregate and Compute

1. `totalPortfolioValue` = sum of all `currentValueBase`
2. `weightPct` for each holding = `(currentValueBase / totalPortfolioValue) Γ— 100`
3. `allocationByAssetClass` = sum of `weightPct` grouped by `assetClass`
4. `allocationByGeography` = sum of `weightPct` grouped by `geography`
5. `allocationByCurrency` = sum of `weightPct` grouped by `currency`
6. `totalUnrealizedPnl` = sum of all `unrealizedPnlBase`
7. `totalCostBasis` = sum of all `costBasisBase` (exclude manual entries without cost basis)
8. `totalReturnPct` = `(totalUnrealizedPnl / totalCostBasis) Γ— 100`
9. `drift` = for each target dimension: `actual% - target%`
10. `concentrationFlags` = any holding where `weightPct > concentrationWarningPct`

### Step 8 β€” Write Snapshot and Brief

Write JSON snapshot to `data/portfolio/snapshots/YYYY-MM-DD-portfolio.json` (date = today).
Write brief to `outputs/portfolio/brief.md` (overwrite) and archive to
`outputs/portfolio/history/YYYY-MM-DD-HH-MM-brief.md`.

---

## Output Templates

### FULL_BRIEF

```
πŸ“Š *Portfolio Brief* β€” {DD Mon YYYY}
FX as of {HH:MM UTC} ({N}h ago){stale_warning}

πŸ’° *Total Value*
${totalValueBase} {baseCurrency}

πŸ“ˆ *Unrealized P&L*
+${pnlBase} (+{pnlPct}%)
Cost basis: ${costBasisBase}

---

🏦 *Asset Allocation*
{asset class rows β€” see format below}

---

🌍 *Geography*
{geography rows}

---

πŸ’± *Currency Exposure*
{currency rows}

---

πŸ” *Top Holdings* (by value)
{top 10 holdings list}

---

{alerts block β€” only if alerts exist}

πŸ’Ύ Snapshot saved.
Sources: {source list with dates}
```

**Allocation row format:**

```
Equity       61% βœ… (tgt 60%)
Fixed Inc    14% ⚠️ (tgt 15%, -1%)
```

- `βœ…` = `|drift| <= driftWarningPct`
- `⚠️` = `|drift| > driftWarningPct`
- Show drift only if non-zero

**Holdings list format:**

```
1. AAPL      3.3%  +23% 🟒
2. SPY       8.1%  +14% 🟒
3. SG Condo 11.2%  +18% 🟒
```

- `🟒` P&L positive, `πŸ”΄` P&L negative, `⬜` no cost basis

**Alerts block:**

```
⚠️ *Alerts*
β€’ {asset class} overweight by {N}%
β€’ {holding name} concentration: {pct}%
```

**Source list:**

```
Sources: IBKR ({date}), Zerodha ({date}),
  Manual ({date}), FX live
```

### QUICK_VALUE

```
πŸ’° *Portfolio Value* β€” {date}
${totalValue} {baseCurrency}

πŸ“ˆ P&L: +${pnl} (+{pct}%)
FX: {age} ago{stale_warning}
```

### ALLOCATION_CHECK

```
🏦 *Allocation vs Targets* β€” {date}

Asset Class:
{rows with βœ…/⚠️}

Geography:
{rows with βœ…/⚠️}

Currency:
{rows with βœ…/⚠️}

{drift summary: "3 dimensions within target" or list warnings}
```

### FX_EXPOSURE

```
πŸ’± *Currency Exposure* β€” {date}

{Currency}  {actual%}  {target%}  {drift%} {symbol}

Largest exposure: {currency} at {pct}%
FX rates: {age} (source: frankfurter.app)
```

### HOLDINGS_DETAIL

Group by asset class. Within each group, sort by `currentValueBase` descending.
Show up to 15 holdings total.

```
πŸ“‹ *Holdings* β€” {date}
Total: ${totalValue} {baseCurrency}

*Equity ({N} positions)*
β€’ AAPL: $9,250 (3.3%) +23% 🟒
β€’ SPY:  $23,100 (8.1%) +14% 🟒

*Fixed Income ({N} positions)*
β€’ US Treasuries ETF: $8,200 (2.9%) +2% 🟒

*Real Estate (manual)*
β€’ SG Condo: $31,900 (11.2%) β€” as of 1 Mar

*Cash*
β€’ DBS SGD: $9,400 (3.3%)
β€’ SBI INR: $3,000 (1.1%)
```

### CONCENTRATION

```
🎯 *Concentration Check* β€” {date}
Threshold: >{threshold}%

{if none flagged}
βœ… No single holding exceeds {threshold}%.
Largest: {name} at {pct}%

{if flagged}
⚠️ Flagged positions:
β€’ {name}: {pct}% (excess: +{N}%)

Top 5 by weight:
1. {name}: {pct}%
...
```

### PNL_SUMMARY

```
πŸ“ˆ *P&L Summary* β€” {date}

Total Unrealized: +${pnl} (+{pct}%)
Cost Basis: ${costBasis}
Current Value: ${totalValue}

By Asset Class:
β€’ Equity:    +${pnl} (+{pct}%)
β€’ Fixed Inc: +${pnl} (+{pct}%)
β€’ Real Est:  +${pnl} (+{pct}%)
β€’ Alts:      +${pnl} (+{pct}%)

{if any realized P&L from trades.csv}
Recent Realized (30d): +${realizedPnl}
```

### REBALANCE_CHECK

```
βš–οΈ *Rebalancing Check* β€” {date}

{if all within threshold}
βœ… Portfolio within target bands.
Largest drift: {dimension} {actual}% vs {target}%

{if drift detected}
⚠️ Drift detected:

Asset Class:
β€’ {name}: {actual}% vs {target}% β†’ {direction} by ${amount}

Geography:
β€’ ...

Currency:
β€’ ...

No specific trades recommended β€” review before acting.
```

### ADD_MANUAL

Prompt the user for the required fields in structured order:

1. Entry type: property / alternative / cash?
2. For property: label, geography, currency, current value, loan outstanding, cost basis, purchase date
3. For alternative: label, geography, currency, current value, cost basis, investment date, notes
4. For cash: label, currency, balance, account type (savings/current/money market)

Write the entry to the appropriate `manual/*.json` file. Confirm with:

```
βœ… Added: {label}
Value: ${valueBase} {baseCurrency}
File: manual/{type}.json
```

### HELP

```
πŸ“Š *Portfolio Intel β€” Commands*

β€’ portfolio brief β€” full snapshot
β€’ portfolio value β€” quick total
β€’ portfolio allocation β€” drift check
β€’ fx exposure β€” currency breakdown
β€’ show holdings β€” position list
β€’ concentration risk β€” overweight check
β€’ portfolio P&L β€” gains/losses
β€’ portfolio refresh β€” re-ingest data
β€’ rebalancing check β€” what to adjust
β€’ add property / add alternative / update cash β€” manual entries

Data freshness:
β€’ IBKR: upload Flex Query CSV to
  data/portfolio/ibkr/positions.csv
β€’ India: upload to
  data/portfolio/india/zerodha-holdings.csv
β€’ Manual: edit data/portfolio/manual/*.json
```

---

## Error Handling

| Situation                      | Action                                                          |
| ------------------------------ | --------------------------------------------------------------- |
| `config.json` missing          | Stop. Tell user to create it from the template.                 |
| IBKR CSV missing               | Continue without IBKR. Note in sources list.                    |
| Indian CSV missing             | Continue without India. Note in sources list.                   |
| Manual file missing            | Skip that file type. Note in sources list.                      |
| FX fetch fails                 | Use cached rates. Add `⚠️ FX stale` to output.                  |
| Both IBKR and India missing    | Ask user to upload data first.                                  |
| Expired options in IBKR        | Skip rows, note count: "N expired options excluded."            |
| Property `updatedAt` > 30 days | Add note: "⚠️ Property value as of {date} β€” consider updating." |

---

## Quality Rules

- Never invent holdings or values. If data is missing, say so.
- Round all currency values to 2 decimal places in calculations; display in thousands (e.g., `$284.5k`) for values > $10,000.
- Round percentages to 1 decimal place.
- Always show data freshness (when was each source file last modified).
- Keep each Telegram message under 4,096 characters. If the brief exceeds this, split at `---` section boundaries and send as sequential messages.
- Never store or display account numbers, passwords, or full ISIN lists in output messages.

Files in this skill

  • SKILL.md14.5 KB
  • references/asset-taxonomy.md6.2 KB
  • references/cas-mf-fields.md7.2 KB
  • references/ibkr-flex-fields.md9 KB
  • references/zerodha-fields.md5.4 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…