vulcan-portfolio-intel
Use this skill when the user asks any of:
- "what's my portfolio / account / position state"
- "check my portfolio / margin / pnl / positions"
- "how am I doing on Phoenix / Vulcan"
- daily / weekly recap requests
Always load this skill first for portfolio questions — do not improvise the layout.
Active Wallet
The snapshot reflects exactly one wallet at a time, resolved in this priority:
ctx.session_wallet— the MCP session wallet unlocked at server start (VULCAN_WALLET_NAME, falling back to the configured default).--wallet <name>override (CLI only).- Default wallet from
vulcan wallet set-default.
Within that wallet, the snapshot covers cross-margin (subaccount 0) and every isolated subaccount. If the user has multiple wallets, the active one is whatever the MCP server was started with; call vulcan_status if you need to confirm wallet.source and wallet.public_key.
Primary Call
Prefer the combined snapshot. It returns margin (cross), positions (all subaccounts), resting orders (cross), isolated subaccount summaries, and account totals in one call:
vulcan_portfolio → {}
Use granular calls only for a single section or symbol-level filtering:
vulcan_margin_status → {} # cross-margin only
vulcan_position_list → {} # all subaccounts; includes uPnL % and initial margin per position
vulcan_trade_orders → { symbol? }
Optional Performance Context
When the user asks for daily, weekly, or historical performance — not for every portfolio check — pull realized PnL history:
vulcan_history → { type: "pnl", limit: 50 }
Sum fills since the cutoff (24h or 7d) to report realized PnL. If history is empty or the call fails, say so explicitly; do not invent a number. Open-position uPnL is not daily/weekly performance — keep them separate.
For agent-session activity, use:
vulcan://agent/position-report
vulcan://agent/session-summary
State the basis for any "win rate" figure — currently open-position win rate from unrealized PnL, not realized closed-trade history.
Field Map
vulcan_portfolio returns:
margin(cross subaccount 0):collateral_balance— USDC parked in cross.effective_collateral— collateral + discounted uPnL + unsettled funding.portfolio_value— collateral + uPnL (cross only).unrealized_pnl,initial_margin,maintenance_margin.risk_state(Safe / AtRisk / Cancellable / Liquidatable / BackstopLiquidatable / HighRisk).risk_tier.available_to_withdraw.num_positions,num_open_orders.
totals(cross + every isolated):total_collateral,total_account_value,total_unrealized_pnl,num_subaccounts.- Always prefer
total_account_valueover cross-onlyportfolio_valuewhen reporting "account value" — otherwise collateral parked in isolated subaccounts is invisible.
isolated_subaccounts[]:subaccount_index,symbol,collateral_balance,effective_collateral,portfolio_value,unrealized_pnl,initial_margin,maintenance_margin.
positions[]:symbol(suffix[iso]for isolated),side,size,entry_price,mark_price.unrealized_pnl,unrealized_pnl_pct— already computed using the phoenix-v2-frontend formula (see below).initial_margin,maintenance_margin,liquidation_price.subaccount_index,subaccount_collateral— present only for isolated.
orders[]— resting limit orders plus conditional TP/SL trigger legs.
Unrealized PnL % (Phoenix-v2-frontend formula)
The field is precomputed; trust unrealized_pnl_pct. For reference, the formula is:
- Isolated:
uPnL / subaccount_collateral. - Cross:
uPnL / entry_initial_margin, whereentry_initial_margin = initial_margin * entry_price / mark_price(rebases current initial margin back to entry).
Returned as a signed percent string, e.g. +1.45%, -3.20%, or — when the denominator is zero / missing.
Presentation Template
Use this exact layout. Adapt only when sections are empty (skip them). Use the listed emojis; do not substitute.
📊 Portfolio Snapshot — <wallet name or short pubkey>
🏦 Account
- Risk: <risk_state> / <risk_tier> [🟢 Safe • 🟡 AtRisk/Cancellable • 🔴 Liquidatable/HighRisk]
- Total account value: $<total_account_value> (cross + isolated)
- Total collateral: $<total_collateral>
- Total uPnL: <signed $total_unrealized_pnl>
- Free to withdraw: $<available_to_withdraw> (cross)
- Cross initial / maintenance margin: $<initial_margin> / $<maintenance_margin>
📈 Positions (<n>)
| Market | Side | Size | Entry | Mark | uPnL | uPnL % | Liq |
| ... | ... | ... | ... | ... | ... | ... | ... |
For each isolated row also note: `iso collateral $X · init margin $Y` underneath the table or in a follow-up bullet.
🧾 Open Orders (<n>) — group by symbol
- <symbol>: <side> <size> @ <price> (<type>) ← regular limit
- <symbol>: TP / SL <side> <size> @ <price> (reduce-only) ← conditional trigger
💸 Funding (only when a position has non-trivial funding exposure)
- <symbol>: <rate>%/8h — <"longs pay shorts" or "shorts pay longs"> · costs/credits you ~$X/day at current size
📅 Performance (only when user asked for daily/weekly, or when realized PnL > 0 was observed this session)
- 24h realized PnL: <$value> (from vulcan_history type=pnl)
- 7d realized PnL: <$value>
🛡️ Notes
- Flag missing TP/SL on positions where the user has not opted out.
- Flag tight liquidation distance (< 5% from mark).
- Do **not** flag laddered TP/SL whose sizes sum to ≤ position size — that is intentional.
Rules:
- The "Account" block uses
totals.total_account_value, notmargin.portfolio_value. The crossportfolio_valueis misleading when isolated subaccounts hold collateral. - Always show
uPnL %next touPnLin the positions table. - For each isolated position, surface its
subaccount_collateralandinitial_margin— those values explain the liquidation price and the cross/total gap. - Funding section is conditional. Skip it for positions where
|funding_rate| < 0.001AND there's no large exposure. Usevulcan_market_tickerper held symbol when including funding. - Performance section is conditional. Skip it unless the user asked or there was a meaningful realized event.
Risk State Decoration
Map risk_state to one indicator emoji:
Safe→ 🟢AtRisk/Cancellable→ 🟡Liquidatable/BackstopLiquidatable/HighRisk→ 🔴
Use the same emoji once next to the risk line; do not pepper it through the rest of the snapshot.
Reduce-Only TP/SL — Do The Math Before Warning
Multiple reduce-only TP or SL orders on the same position are almost always a laddered exit, not a bug:
- Sum sizes of reduce-only TP orders. Same for SL.
- Compare to position size.
- Only flag if a side's sum exceeds position size (excess no-ops due to reduce-only) or direction is wrong (long SL above entry, long TP below entry, etc.).
A long 2.00 SOL with two reduce-only SL sells of 1.00 each → sums to 2.00 → exact match → valid laddered SL, do not warn.
When To Hand Off
- User asks to close a position →
vulcan-position-management(confirm first, orderbook check for large closes). - User asks to add/withdraw collateral, transfer between subaccounts →
vulcan-margin-operations. - User wants to open a new position →
vulcan-trade-execution+vulcan-risk-management.