Two returns, one ratio.
Every return starts from the gross return g: a price divided by an earlier price. The log-return takes its natural logarithm; the simple return subtracts one.
Log-returns add up over time
The log-returns of consecutive periods sum to the log-return of the whole span, ln(last price / first price): compounding multiplies the gross returns, and the logarithm turns the product into a sum. That is why k-period returns and volatilities are usually built from them.
Simple returns add up across assets
The simple return of a portfolio is the weighted average of the simple returns of its assets. Log-returns do not have this property.
The first example of the running sums and products workbook shows the first property with monthly returns of three funds (sheet By column): the running product of the gross returns gives the growth of 1 invested, the running sum of the log-returns gives the cumulative log-return, and the exponential of the sum equals the product.
=fx_CumProduct(1 + n_Returns) growth of 1 invested: running product of g
=fx_CumSum(LN(1 + n_Returns)) cumulative log-return: running sum of r = ln(g)
=MAX(ABS(EXP(L9#) - H9#)) check: 4.4E-16, EXP of the sum = the productFor small moves the two are close and the gap grows with the size of the move: in the workbook, a simple return of −5.04% is a log-return of −5.17%.
A column of prices, and a name that follows it.
Type or paste the closing prices in one column, oldest on top; in the workbook they start in C9. Then give the column a name in Formulas > Name Manager, with the trim operator :., and a second name for its length:
d_HistPrices ='LogReturns (1 period)'!$C$9:.$C$1000
n_NObs =ROWS(d_HistPrices)C9:.C1000 is the range C9:C1000 without its trailing empty cells, the same as TRIMRANGE(C9:C1000, 2). The name therefore ends at the last price, whatever the length of the series, and grows when you add prices below. Every formula on this page uses d_HistPrices and n_NObs, so none of them has to change when the data do. The prefixes follow one convention across the series: d_ for a dynamic range, one that changes size (a spill or a trimmed range such as this one), n_ for every other name (inputs, constants, single results), fx_ for LAMBDA functions.
Keep the column empty below the prices. Anything typed there, even a short note, becomes part of the range: the series then contains text and the returns come out wrong or as errors.
The trim operators need Excel for Microsoft 365 or Excel for the web. The prices can come from any source: a download, a data provider or a table copied from a report.
Drop the first, drop the last.
With n prices there are n − 1 one-period returns. The numerator is the price column without its first price, the denominator the same column without its last price: the two are aligned row by row, so one division gives every ratio.
=LET(Prices_, d_HistPrices,
LN(DROP(Prices_, 1) / DROP(Prices_, -1)))LET gives the column the short name Prices_, the same name fx_LogReturn and fx_Return use for their argument, so the formula already reads like the function it becomes in section 05. For simple returns, drop the logarithm and subtract one: =LET(Prices_, d_HistPrices, DROP(Prices_, 1) / DROP(Prices_, -1) - 1).
Seven formulas, one rule
There are several ways to carry out the same calculation dynamically, with older Excel functions (INDEX, OFFSET) and newer ones (DROP, TAKE, SEQUENCE, LAMBDA). Here are the possible choices; our preference is the first one, LN + DROP.
| Method | Formula |
|---|---|
| LN + DROP No count needed: the clearest form of the rule. | =LET(Prices_, d_HistPrices, LN(DROP(Prices_, 1) / DROP(Prices_, -1))) |
| 2 × INDEX Two INDEX joined by a colon return a range. | =LET(Prices_, d_HistPrices, LN(INDEX(Prices_, 2, 1):INDEX(Prices_, n_NObs, 1) / INDEX(Prices_, 1, 1):INDEX(Prices_, n_NObs - 1, 1))) |
| INDEX + SEQUENCE The positions as an array: the idea behind the non-overlapping case. | =LET(Prices_, d_HistPrices, LN(INDEX(Prices_, SEQUENCE(n_NObs - 1, 1, 2)) / INDEX(Prices_, SEQUENCE(n_NObs - 1)))) |
| OFFSET Volatile: recalculates at every change in the workbook. | =LET(Prices_, d_HistPrices, LN(OFFSET(Prices_, 1, 0, n_NObs - 1, 1) / OFFSET(Prices_, 0, 0, n_NObs - 1, 1))) |
| TAKE The mirror image of DROP: keep instead of remove. | =LET(Prices_, d_HistPrices, LN(TAKE(Prices_, 1 - n_NObs) / TAKE(Prices_, n_NObs - 1))) |
| LAMBDA in a cell Try a function in a cell before naming it. | =LAMBDA(Prices_, LET(num_, DROP(Prices_, 1), den_, DROP(Prices_, -1), LN(num_ / den_)))(d_HistPrices) |
| fx_LogReturn The same rule, with a name. | =fx_LogReturn(d_HistPrices) |
DROP and TAKE need neither the number of prices nor a fixed size; INDEX and OFFSET need n_NObs.
Overlapping windows or non-overlapping blocks.
A weekly return from daily prices spans k = 5 periods; a monthly one about 21. There are two ways to cut the series into k-period returns.
Overlapping: shift by k
Shift by k instead of 1. Every price but the last k starts a window, so n prices give n − k returns, and consecutive returns share k − 1 periods:
=LET(Prices_, d_HistPrices,
LN(DROP(Prices_, n_k) / DROP(Prices_, -n_k)))Overlapping returns use all the data, but neighbouring returns are strongly correlated even when the one-period returns are not: statistics that assume independent observations, such as the usual standard errors, look more precise than they are.
Non-overlapping: blocks anchored at the latest price
Blocks that do not overlap give m = ROUNDDOWN((n − 1) / k, 0) returns. When k does not divide n − 1, q = MOD(n − 1, k) periods are left over. The workbook puts them at the start, so that the last return always ends on the latest price: the end positions are n, n − k, n − 2k, …, and SORT puts them back in date order.
=LET(
Prices_, d_HistPrices,
n_, n_NObs,
m_, ROUNDDOWN((n_ - 1) / n_k, 0),
endIx_, SORT(SEQUENCE(m_, 1, n_, -n_k)),
LN(INDEX(Prices_, endIx_) / INDEX(Prices_, endIx_ - n_k))
)With 22 prices and k = 6, the blocks end at 10, 16 and 22 and the first three prices are not used. Anchoring at the first price instead would end the blocks at 7, 13 and 19 and leave out the three most recent prices, usually the ones that matter most.
fx_Return: one function for every case.
fx_Return(Prices_, [k_], [overlap_], [log_]) brings the two branches and the two kinds of return together. The optional arguments default to one period, overlapping windows and log-returns, so =fx_Return(d_HistPrices) equals fx_LogReturn. The flags accept TRUE/FALSE or 1/0.
| Formula | Result |
|---|---|
=fx_Return(d_HistPrices) | One-period log-returns, the same as fx_LogReturn |
=fx_Return(d_HistPrices, 5) | Overlapping 5-period log-returns: weekly from daily prices |
=fx_Return(d_HistPrices, 5, FALSE) | Non-overlapping 5-period log-returns |
=fx_Return(d_HistPrices, 21, 0, 0) | Non-overlapping 21-period simple returns: monthly from daily prices |
The two functions are independent: define either one on its own. The workbook has both. How to add a LAMBDA in Name Manager.
fx_LogReturn: the complete commented function
=LAMBDA(Prices_,
LET(
Text1_, "Prices_: one column, oldest price on top",
Text2_, "Shift rule: drop the first price (numerator), drop the last price (denominator)",
num_, DROP(Prices_, 1),
den_, DROP(Prices_, -1),
Text3_, "One-period log-returns: n - 1 values, in date order",
LN(num_ / den_)
)
)fx_Return: the complete commented function
=LAMBDA(Prices_, [k_], [overlap_], [log_],
LET(
Text1_, "Defaults: one period, overlapping windows, log-returns (flags accept 1/0 or TRUE/FALSE)",
h_, IF(ISOMITTED(k_), 1, k_),
ov_, IF(ISOMITTED(overlap_), TRUE, overlap_),
lg_, IF(ISOMITTED(log_), TRUE, log_),
Text2_, "Non-overlapping blocks end at n, n-k, n-2k, ...: anchored at the latest price, sorted into date order",
n_, ROWS(Prices_),
endIx_, SORT(SEQUENCE(ROUNDDOWN((n_ - 1) / h_, 0), 1, n_, -h_)),
Text3_, "Shift rule: numerator = later prices, denominator = prices k periods earlier",
num_, IF(ov_, DROP(Prices_, h_), INDEX(Prices_, endIx_, 1)),
den_, IF(ov_, DROP(Prices_, -h_), INDEX(Prices_, endIx_ - h_, 1)),
Text4_, "Gross return, then log-return or simple return",
gross_, num_ / den_,
IF(lg_, LN(gross_), gross_ - 1)
)
)The slides.
The slides of the lesson, by Gianluca Fusai, Luca Alberici and Marco Migliardi, cover the same ground in more detail: log and simple returns, the shift rule and its alternatives, overlapping and non-overlapping windows on a small example, and the two LAMBDA functions.
PDF · 34 slides · 0.35 MB · no email needed
Paste your own prices.
Download the workbook, inspect the formulas and replace the prices with your own series. It contains:
- LogReturns (1 period): the seven formulas of Fig. 01, the simple return and the name with the trim operator.
- LogReturns (k period Overlap): overlapping windows, step by step.
- LogReturns (k period not Overl): non-overlapping blocks anchored at the latest price, with the leftover periods in grey.
- fx_Return: the four cases, your own choice of k and flags, and four checks.
- LAMBDA code: fx_LogReturn and fx_Return, commented and already installed.
Enter your email to unlock an immediate browser download. You can also choose to hear about new lessons; it is optional.
Tested in Excel for Microsoft 365 on Windows (desktop). Excel for Mac and Excel for the web support the same functions but were not tested.
What changed
- 1.0 · 8 November 2026. First version.
Suggest an improvement or a new problem.
Spotted something that could be clearer, or a pricing, risk or portfolio problem you would like to see solved in Excel? Send it here: reader suggestions shape the next lessons.