Cumulative Product for Random Walk
Calculate the cumulative product of daily returns to simulate a stock's price path (random walk). This is useful for financial modeling, backtesting trading strategies, and portfolio analysis where you need to compound returns over time.
Problem: Compound Daily Returns
You have a table with daily returns for a stock and want to calculate the cumulative price starting from an initial value (e.g., $100). Each day's price is calculated by multiplying the previous price by (1 + return).
For example, with these daily returns:
| Date | Daily Return (%) |
|---|---|
| 2024-09-05 | 2.00 |
| 2024-09-06 | -1.00 |
| 2024-09-07 | 1.50 |
| 2024-09-08 | -3.00 |
You want to calculate:
| Date | Daily Return (%) | Stock Price |
|---|---|---|
| 2024-09-05 | 2.00 | 102.00 |
| 2024-09-06 | -1.00 | 100.98 |
| 2024-09-07 | 1.50 | 102.49 |
| 2024-09-08 | -3.00 | 99.42 |
Solution: Use Logarithm Mathematics Trick
Since QuestDB doesn't allow functions on top of window function results, we use a mathematical trick: the exponential of the sum of logarithms equals the product.
WITH ln_values AS (
SELECT
date,
return,
SUM(ln(1 + return)) OVER (ORDER BY date) AS ln_value
FROM daily_returns
)
SELECT
date,
return,
100 * exp(ln_value) AS stock_price
FROM ln_values;
This query:
- Calculates
ln(1 + return)for each day - Uses a cumulative
SUMwindow function to add up the logarithms - Applies
exp()to convert back to the product
How It Works
The mathematical identity used here is:
product(1 + r₁, 1 + r₂, ..., 1 + rₙ) = exp(sum(ln(1 + r₁), ln(1 + r₂), ..., ln(1 + rₙ)))
Breaking it down:
ln(1 + return)converts each multiplicative factor to an additive oneSUM(...) OVER (ORDER BY date)creates a cumulative sumexp(ln_value)converts the cumulative sum back to a cumulative product- Multiply by 100 to apply the starting price of $100
Why This Works
QuestDB doesn't support direct window functions like PRODUCT() OVER(), and attempting exp(SUM(ln(1 + return)) OVER ()) fails with a "dangling literal" error because you can't nest functions around window functions.
The workaround is to use a CTE to compute the cumulative sum first, then apply exp() in the outer query where it's operating on a regular column, not a window function result.
Adapting to Your Data
You can easily modify this pattern:
Different starting price:
SELECT date, return, 1000 * exp(ln_value) AS stock_price -- Start at $1000
FROM ln_values;
Different time granularity:
-- For hourly returns
WITH ln_values AS (
SELECT
timestamp,
return,
SUM(ln(1 + return)) OVER (ORDER BY timestamp) AS ln_value
FROM hourly_returns
)
SELECT timestamp, 100 * exp(ln_value) AS price FROM ln_values;
Multiple assets:
WITH ln_values AS (
SELECT
date,
symbol,
return,
SUM(ln(1 + return)) OVER (PARTITION BY symbol ORDER BY date) AS ln_value
FROM daily_returns
)
SELECT
date,
symbol,
100 * exp(ln_value) AS stock_price
FROM ln_values;
This pattern is essential for Monte Carlo simulations in finance. Generate random returns, apply this cumulative product calculation, and run thousands of iterations to model possible future price paths.