analyze_factors
Analyze portfolio factor exposures including market beta, asset class contributions, regional and sector breakdowns, interest rate sensitivity, dividend yield, and currency exposure to assess diversification and risk.
Instructions
Factor exposure analysis: portfolio market beta, asset-class contributions, region/sector breakdown, interest-rate sensitivity (duration), dividend yield, currency exposure. Payment: $0.02 USDC on Tempo chain.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| holdings | Yes | ||
| profile | No | Risk profile — affects rebalance targets and scoring. Default: balanced. | |
| benchmarkReturn | No | Annual benchmark return for Sharpe calculation, e.g. 0.08 = 8%. Default: 0.08. | |
| riskFreeRate | No | Annual risk-free rate for Sortino and VaR excess return, e.g. 0.05 = 5%. Default: 0.05. | |
| rebalanceMethod | No | Portfolio construction method for rebalance recommendations. Default: profile. | |
| marketIndicators | No | Optional macro indicators — improves market regime detection confidence to HIGH when 3+ provided. |
Implementation Reference
- src/analyze/factor-exposure.ts:66-158 (handler)The main handler function for factor exposure analysis, calculating beta, asset class contributions, duration, and other risk factors.
export function analyzeFactorExposure( portfolio: Portfolio, returnSeries?: Record<string, number[]>, ): FactorExposureResult { const holdings = portfolio.holdings // ── Market beta (real OLS regression when available, class default otherwise) ─ const marketBeta = holdings.reduce( (acc, h) => acc + h.weight * resolveEffectiveBeta(h, returnSeries), 0, ) let betaInterpretation: string if (marketBeta < 0.2) { betaInterpretation = 'Defensive — portfolio moves far less than the broad market' } else if (marketBeta < 0.7) { betaInterpretation = 'Low-beta — underperforms in bull markets, outperforms in downturns' } else if (marketBeta < 1.2) { betaInterpretation = 'Market-neutral — tracks broad market closely' } else if (marketBeta < 1.8) { betaInterpretation = 'High-beta — amplified market moves in both directions' } else { betaInterpretation = 'Very high-beta — extreme market sensitivity' } // ── Asset class contributions ──────────────────────────────────────────── const classMap = new Map< AssetClass, { weight: number; betaContrib: number; volContrib: number } >() for (const h of holdings) { const beta = resolveEffectiveBeta(h, returnSeries) const existing = classMap.get(h.assetClass) ?? { weight: 0, betaContrib: 0, volContrib: 0 } classMap.set(h.assetClass, { weight: existing.weight + h.weight, betaContrib: existing.betaContrib + h.weight * beta, volContrib: existing.volContrib + h.weight * h.volatility, }) } const assetClassContributions: AssetClassContribution[] = Array.from(classMap.entries()) .map(([assetClass, { weight, betaContrib, volContrib }]) => ({ assetClass, weight: Number(weight.toFixed(4)), betaContribution: Number(betaContrib.toFixed(4)), volContribution: Number(volContrib.toFixed(4)), })) .sort((a, b) => b.weight - a.weight) // ── Region breakdown ───────────────────────────────────────────────────── const regionBreakdown = groupBy( holdings.map((h) => ({ key: h.region as Region, weight: h.weight })), ).map(({ key, weight }) => ({ region: key, weight })) // ── Sector breakdown ───────────────────────────────────────────────────── const sectorBreakdown = groupBy( holdings.map((h) => ({ key: (h.sector ?? h.assetClass) as string, weight: h.weight, })), ).map(({ key, weight }) => ({ sector: key, weight })) // ── Fixed-income metrics ───────────────────────────────────────────────── const portfolioDuration = Number( holdings.reduce((acc, h) => acc + h.weight * h.duration, 0).toFixed(2), ) const weightedDividendYield = Number( holdings.reduce((acc, h) => acc + h.weight * h.dividendYield, 0).toFixed(4), ) const bondWeight = holdings .filter((h) => h.assetClass === 'bond') .reduce((acc, h) => acc + h.weight, 0) const reWeight = holdings .filter((h) => h.assetClass === 'real_estate') .reduce((acc, h) => acc + h.weight, 0) // ── Currency exposure ──────────────────────────────────────────────────── const currencyExposure = groupBy( holdings.map((h) => ({ key: h.currency, weight: h.weight })), ).map(({ key, weight }) => ({ currency: key, weight })) return { marketBeta: Number(marketBeta.toFixed(4)), betaInterpretation, assetClassContributions, regionBreakdown, sectorBreakdown, portfolioDuration, weightedDividendYield, interestRateSensitivity: interestRateSensitivity(portfolioDuration, bondWeight, reWeight), currencyExposure, } } - src/index.ts:572-579 (registration)The MCP/API tool registration for 'analyze_factors' (via the /analyze/factors route).
app.post('/analyze/factors', validate, charge('0.01'), async (c) => { try { return c.json(withMeta(c, analyzeFactorExposure(c.get('portfolio'), c.get('returnSeries')))) } catch (err) { console.error(err) return internalError(c) } })