You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

9.7 KiB

Volatility Estimate + Cash Allocation — Scoring Logic Spec

Author: sesha siva sankar (member 1) Date: Fri, Sep 18 Task: Spec volatility estimate + cash allocation scoring logic


1. Two separate checks, bundled in one doc

These are two different signals that don't share a formula (unlike Thursday's concentration rules, which were all one formula). I'm speccing them together because they're on the same day of the plan, not because they're related:

  • Volatility estimate — how bumpy is this portfolio likely to be, based on price history?
  • Cash allocation — is too much (or too little) of the portfolio just sitting in cash?

2. Volatility Estimate

2.1 What data we actually have to work with

Checked the schema — MarketData (prisma/schema.prisma:161) stores one row per (dataSource, symbol, date) with a marketPrice. That's daily closing prices per symbol, already being collected for every symbol in a user's holdings (it's how the performance charts work today). So there's no new data collection needed — this is a calculation on top of existing price history, same relationship as concentration risk was to assetProfile.

2.2 The formula

Standard approach, nothing exotic — annualized volatility from daily returns:

1. For each holding, pull the last N days of MarketData.marketPrice (I'd default N = 90
   trading days -- long enough to be stable, short enough to reflect current risk, not
   ancient history).
2. Compute daily returns: r[i] = ( price[i] - price[i-1] ) / price[i-1]
3. Compute the standard deviation of those daily returns -> dailyVolatility
4. Annualize it: annualizedVolatility = dailyVolatility × sqrt(252)
   (252 = approx. number of trading days in a year -- standard convention)
5. That gives per-holding volatility. To get PORTFOLIO volatility (not just an average of
   the holdings), we need the weighted combination accounting for how holdings move
   together:

   portfolioVolatility = sqrt( Σ Σ w_i × w_j × cov(i, j) )

   where w_i is holding i's weight (% of portfolio) and cov(i,j) is the covariance between
   holding i and j's daily returns (cov(i,i) is just that holding's own variance).

2.3 Why the covariance step matters (and can't be skipped)

If I just averaged each holding's own volatility weighted by portfolio %, that would overstate risk for a diversified portfolio — two volatile stocks that move in opposite directions cancel each other out and the portfolio is calmer than either stock alone. This is literally the mathematical reason diversification works. Skipping it would make the score effectively useless for rewarding diversification, so it's worth the extra complexity even though it's the harder part to implement (an N×N covariance matrix instead of N single numbers).

2.4 Turning volatility into a pass/fail (or a scaled score)

Unlike concentration (single ceiling), volatility should probably be a band, same pattern as the Asset Class / Market cluster rules from Tuesday's notes — because both too high AND too low can be "wrong" depending on the user's risk profile:

  • Default band: 10%-25% annualized volatility (roughly "balanced portfolio" territory — pure bonds sit lower, all-crypto sits way higher).
  • Important open question: this really should key off the user's declared risk tolerance (if Ghostfolio has that setting) rather than one fixed band for everyone — a 25-year-old aggressive investor and someone near retirement shouldn't be held to the same volatility target. Flagging this for the team rather than deciding solo, since I didn't find a risk-tolerance field on the user profile during my read-through — that might need its own small spec if we want to do this properly.
  • Output shape matches every other rule (EvaluationResult): value: false, evaluation: "Your portfolio's estimated annualized volatility is 31.4%, above the 25% upper band.", plus the raw number so the UI can show a proper gauge, not just pass/fail.

2.5 Performance note (implementation concern, not a design one)

Computing an N×N covariance matrix on every X-Ray request for a portfolio with, say, 50 holdings is 2,500 covariance calculations, each needing 90 days of price history pulled per pair. That's expensive to do live on every page load. Recommending this be precomputed/cached (e.g. nightly job, same idea as other market-data refresh jobs in this codebase) rather than calculated synchronously in the rule's evaluate() — flagging this now so it doesn't get missed when someone picks this up for implementation.

3. Cash Allocation Scoring

3.1 How this differs from the existing "Buying Power" rule

Tuesday's notes already found a BuyingPower rule — but that only checks "is your cash above a minimum floor" (protects against having too little liquidity). It says nothing about the other failure mode: too much cash sitting uninvested, which is its own drag on long-term returns (cash loses to inflation over time). So cash allocation is really a band check, same shape as volatility above, not a second liquidity-floor check.

3.2 The formula

cashRatio = totalCashBalance / totalPortfolioValue   (holdings + cash combined)

Fail if cashRatio > thresholdMax  (too much idle cash, e.g. > 15%)
Fail if cashRatio < thresholdMin  (not enough of a buffer, e.g. < 2%)
  • totalCashBalance and portfolio value are both already computed today (this is the same summary.cash figure the BuyingPower rule already reads, per Tuesday's notes on apps/api/src/models/rules/liquidity/buying-power.ts) — no new data needed here either.
  • Default band: 2%-15%. Below 2% and the emergency-fund/liquidity checks are likely also already failing (some overlap with Emergency Fund Coverage from Tuesday is expected and fine — they're answering slightly different questions: "do you have money for emergencies" vs "is your invested/cash split efficient").

3.3 Relationship to Buying Power — keep both, don't merge

Worth being explicit about this since it's a natural question: Buying Power (existing) and Cash Allocation (new) sound similar but aren't redundant —

  • Buying Power = "do you have enough cash on hand right now" (an absolute dollar floor, configurable in currency units)
  • Cash Allocation = "is the proportion of cash-to-invested reasonable" (a ratio/band, same shape as every cluster-risk rule)

Someone with $500K invested and $5K cash might pass Buying Power (enough for near-term needs) but still fail Cash Allocation on the low end if their target buffer is higher in percentage terms — that's a legitimate, different signal, not double-counting.

4. Output shape (both, same pattern as every other rule)

{
  "key": "PortfolioVolatility",
  "name": "Volatility Estimate",
  "isActive": true,
  "value": false,
  "evaluation": "Estimated annualized volatility is 31.4%, above the 25% upper band.",
  "configuration": {
    "threshold": {
      "min": { "unit": "%", "value": 0.1 },
      "max": { "unit": "%", "value": 0.25 }
    }
  }
}
{
  "key": "CashAllocation",
  "name": "Cash Allocation",
  "isActive": true,
  "value": true,
  "evaluation": "5.1% of your portfolio is in cash, within the 2%-15% target band.",
  "configuration": {
    "threshold": {
      "min": { "unit": "%", "value": 0.02 },
      "max": { "unit": "%", "value": 0.15 }
    }
  }
}

5. How these fold into the Health Score model (Wednesday's doc)

Both slot in as two more rows in the weight table from 02-health-score-data-model.md:

Category Weight (revised) Why
Volatility Estimate 10 New category — meaningful risk signal, but I'm pulling points from Regional Market Cluster Risk (15 -> ~10-ish split) rather than inventing new total budget, to keep the grand total at 100
Cash Allocation 5 Folds into the existing Liquidity category alongside Buying Power (5+5=10 total for Liquidity) since they're clearly related

Same caveat as Thursday's concentration doc: these are my proposed numbers, not locked — worth a quick team gut-check on final weights once all 4-5 new rules from this week are on the table together, rather than re-balancing the 100-point pie one doc at a time.

6. Edge cases

  • New symbol with < 90 days of price history (recently added holding, or the platform just recently added its data): fall back to whatever history exists (even 20-30 days) rather than failing to compute — flag in the evaluation message that the estimate is "based on limited history" so it doesn't look falsely precise.
  • Single-holding portfolio: covariance matrix degenerates to just that one holding's own variance — formula still works, no special-casing needed.
  • All-cash portfolio (0 holdings): Volatility rule shouldn't run at all (nothing to measure) — same hasOpenHoldings gate as other rules. Cash Allocation, however, SHOULD still run (100% cash is a valid, checkable state — it'd fail the upper band, correctly).

7. What's next

Monday's task is deviation-from-target-allocation logic — that's a genuinely different shape again (comparing actual vs. a user-defined target, not just a fixed threshold), so that'll need its own spec rather than extending this one.