Sheets-native portfolio analytics: returns, risk, factor exposure, and attribution
Disclaimer: Cell references and ranges (e.g.,B3
,B6
,AP22–AP24
, and 3-column blocks likeD:E:F
) may differ in your sheet. Adjust anchors and ranges to match your layout—the formula patterns remain the same.
0.04
)0.05
)=GOOGLEFINANCE("^GSPC","price",$B$3,$B$6)
(spills A=Date, B=Close)=IF(OR(B11="",B10=""),"",B11/B10-1)
=LET(m, FILTER($C$11:$C, ($A$11:$A>=$B$3)*($A$11:$A<=$B$6)*ISNUMBER($C$11:$C)), IF(COUNT(m)<2,"", VAR.P(m)*252))
=IF(OR(E11="",E10=""),"",E11/E10-1)
=IFERROR(
COVARIANCE.P(
FILTER( XLOOKUP(D$11:D, A$11:A, C$11:C, "", -1), (D$11:D>=$B$3)*(D$11:D<=$B$6) ),
FILTER( F$11:F, (D$11:D>=$B$3)*(D$11:D<=$B$6) )
)*252,
"")
=LET(rf, FILTER(F$11:F,(D$11:D>=$B$3)*(D$11:D<=$B$6)*ISNUMBER(F$11:F)), IF(COUNT(rf)<2,"", VAR.P(rf)*252))
=LET(rf, FILTER(F$11:F,(D$11:D>=$B$3)*(D$11:D<=$B$6)*ISNUMBER(F$11:F)), IF(COUNT(rf)<2,"", STDEV.P(rf)*SQRT(252)))
=LET(
covA, IFERROR(
COVARIANCE.P(
FILTER( XLOOKUP(D$11:D, A$11:A, C$11:C, "", -1), (D$11:D>=$B$3)*(D$11:D<=$B$6) ),
FILTER( F$11:F, (D$11:D>=$B$3)*(D$11:D<=$B$6) )
)*252, ""),
IF(OR(covA="", $AP$22<=0),"", covA/$AP$22))
=IF(OR(F6="", $AP$23="", $AP$24=""), "", $AP$23 + F6*$AP$24)
=IFERROR(
XLOOKUP($B$6, D$10:D, E$10:E, "", -1) / XLOOKUP($B$3, D$10:D, E$10:E, "", 1) - 1,
"")
=IF(OR(<ActualCell>="", <ExpectedCell>=""), "", <ActualCell> - <ExpectedCell>)
High-quality analytics are paywalled. This project demonstrates that with careful formula design you can match institutional risk/return workflows: variance, volatility, covariance, beta, CAPM expected return, actual return, alpha, plus portfolio rollups—directly in Google Sheets, with reproducibility and no scripts.
Inputs
JNJ, NVDA, AAPL, XOM, NEE, JPM, XLP, VDC, TGT, COST
(extensible)Data layer
Computation layer
XLOOKUP(..., -1)
.Presentation layer
For any price series P_t
: r_t = P_t / P_{t-1} - 1
. In Sheets, guard for blanks to avoid spurious values.
VAR.P(r)
. Annualize: ×252
.STDEV.P(r)
. Annualize: ×√252
.We pair each stock’s daily return with the market return from the same or nearest previous trading day to handle holidays & gaps:
=IFERROR(
COVARIANCE.P(
FILTER( XLOOKUP(D$11:D, A$11:A, C$11:C, "", -1), (D$11:D>=$B$3)*(D$11:D<=$B$6) ),
FILTER( F$11:F, (D$11:D>=$B$3)*(D$11:D<=$B$6) )
)*252,
"")
β = Cov(stock, market) / Var(market)
, with both terms annualized. Denominator is AP22.
E[r] = R_f + β × MRP
. Inputs in AP23 and AP24.
Use last close on/before B6 divided by first close on/after B3, minus 1.
α = Actual − Expected
. Positive alpha indicates outperformance beyond risk exposure.
XLOOKUP(...,-1)
to prevent FILTER
size mismatches.LET
scoping reduces recomputation; only TODAY
and GOOGLEFINANCE
are volatile.Weights
Daily portfolio returns (aligned to benchmark dates)
=LET(
md, $A$11:$A, /* market dates, row 11 down */
r1, XLOOKUP(md, D$11:D, F$11:F, "", -1),
r2, XLOOKUP(md, G$11:G, I$11:I, "", -1),
r3, XLOOKUP(md, J$11:J, L$11:L, "", -1),
r4, XLOOKUP(md, M$11:M, O$11:O, "", -1),
r5, XLOOKUP(md, P$11:P, R$11:R, "", -1),
r6, XLOOKUP(md, S$11:S, U$11:U, "", -1),
r7, XLOOKUP(md, V$11:V, X$11:X, "", -1),
r8, XLOOKUP(md, Y$11:Y, AA$11:AA, "", -1),
r9, XLOOKUP(md, AB$11:AB, AD$11:AD, "", -1),
r10,XLOOKUP(md, AE$11:AE, AG$11:AG, "", -1),
R, HSTACK(r1,r2,r3,r4,r5,r6,r7,r8,r9,r10),
w, HSTACK(F8,I8,L8,O8,R8,U8,X8,AA8,AD8,AG8),
pr, MMULT(R, TRANSPOSE(w)),
pr)
Portfolio annualized volatility
=LET(pr, <reference_to_portfolio_return_series_above>,
rf, FILTER(pr, ISNUMBER(pr)),
IF(COUNT(rf)<2,"", STDEV.P(rf)*SQRT(252)))
Portfolio actual return over window
=LET(pr, <portfolio_return_series>, rf, FILTER(pr, (md>=$B$3)*(md<=$B$6)*ISNUMBER(pr)),
IF(COUNT(rf)<2,"", PRODUCT(1+rf) - 1))
Sharpe Ratio
=( <PortfolioActual> - $AP$23 ) / <PortfolioVolAnnual>
Information Ratio (proper)
=LET(
md, $A$11:$A,
rm, $C$11:$C, /* market return series */
pr, <portfolio_return_series>,
ex, FILTER(pr - rm, ISNUMBER(pr)*ISNUMBER(rm)),
IF(COUNT(ex)<2,"", (<PortfolioActual> - <BenchmarkActual>) / (STDEV.P(ex)*SQRT(252)))
)
Actual − (Rf + β × MRP)
matches the card calculation.σ_p = √(wᵀ Σ w)
from daily series.XLOOKUP(...,-1)
, then apply a single mask to both arrays.
XLOOKUP(date, dates, values, "", -1)
to return the previous trading day when an exact match doesn’t exist.
AP22 > 0
for Beta, and ensure start price > 0 in Actual Return. Use IF()
gates or IFERROR()
.
=IF(OR(B11="",B10=""),"",B11/B10-1)
=IFERROR(
COVARIANCE.P(
FILTER( XLOOKUP(D$11:D, A$11:A, C$11:C, "", -1), (D$11:D>=$B$3)*(D$11:D<=$B$6) ),
FILTER( F$11:F, (D$11:D>=$B$3)*(D$11:D<=$B$6) )
)*252,
""
)
=LET(rf, FILTER(F$11:F,(D$11:D>=$B$3)*(D$11:D<=$B$6)*ISNUMBER(F$11:F)), IF(COUNT(rf)<2,"", VAR.P(rf)*252))
=LET(rf, FILTER(F$11:F,(D$11:D>=$B$3)*(D$11:D<=$B$6)*ISNUMBER(F$11:F)), IF(COUNT(rf)<2,"", STDEV.P(rf)*SQRT(252)))
=LET(
covA, IFERROR(
COVARIANCE.P(
FILTER( XLOOKUP(D$11:D, A$11:A, C$11:C, "", -1), (D$11:D>=$B$3)*(D$11:D<=$B$6) ),
FILTER( F$11:F, (D$11:D>=$B$3)*(D$11:D<=$B$6) )
)*252, ""),
IF(OR(covA="", $AP$22<=0),"", covA/$AP$22))
=IFERROR(
XLOOKUP($B$6, D$10:D, E$10:E, "", -1) / XLOOKUP($B$3, D$10:D, E$10:E, "", 1) - 1,
""
)
=$AP$23 + <BetaCell> * $AP$24
=<ActualCell> - <ExpectedCell>
Statistical rigor • Defensive spreadsheet engineering • Product thinking