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 ruleszerodha-fields.mdโ Zerodha/Groww field mapping and MF classificationasset-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 filestargets.assetClass,targets.geography,targets.currencyโ percentage targetsalertThresholds.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
ratesobject - Write updated
fx-rates.jsonwith newfetchedAttimestamp - 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:
- Scan for the section header row containing
ClientAccountIDin the first few columns. If the file uses IBKR's multi-section format, rows before the correct header are metadata โ skip them. - Read all data rows in the Open Positions section.
- Skip rows where
AssetClass = CASH(usemanual/cash.jsonfor cash balances instead). - Skip rows where
Expiryis in the past (expired options) โ note the count. - Apply asset class mapping (see taxonomy reference).
- Convert
PositionValue(inCurrency) tobaseCurrencyusing FX rates. - 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/:
Detect format by inspecting the header row:
- Contains
Scheme with Folioโ CAS-style (MF Central / CAMS / KFintech / Value Research) Seereferences/cas-mf-fields.mdfor field mapping and classification rules. - Contains
InstrumentandQty.โ Zerodha Console Seereferences/zerodha-fields.md. - Contains
Fund NameandUnitsโ Groww Seereferences/zerodha-fields.md(Groww section).
- Contains
Apply classification rules from the appropriate reference document.
For CAS-style files: skip the
Grand Total :row and any trailing blank rows. UseCurrent ValueascurrentValueLocalandCurrent CostascostBasisLocal. UseUnrealized Gaindirectly (do not recompute). UseXIRRper fund for return display.Convert INR values to baseCurrency.
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:cashmanual/property.jsonโ usenetValueLocalCcy(after loan), asset class:real_estatemanual/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
geographyfield from the JSON
Step 7 โ Aggregate and Compute
totalPortfolioValue= sum of allcurrentValueBaseweightPctfor each holding =(currentValueBase / totalPortfolioValue) ร 100allocationByAssetClass= sum ofweightPctgrouped byassetClassallocationByGeography= sum ofweightPctgrouped bygeographyallocationByCurrency= sum ofweightPctgrouped bycurrencytotalUnrealizedPnl= sum of allunrealizedPnlBasetotalCostBasis= sum of allcostBasisBase(exclude manual entries without cost basis)totalReturnPct=(totalUnrealizedPnl / totalCostBasis) ร 100drift= for each target dimension:actual% - target%concentrationFlags= any holding whereweightPct > 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:
- Entry type: property / alternative / cash?
- For property: label, geography, currency, current value, loan outstanding, cost basis, purchase date
- For alternative: label, geography, currency, current value, cost basis, investment date, notes
- 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.