Log returns
Log returns measure the relative change between consecutive prices using the natural logarithm. They are preferred over simple returns in financial analysis because they are additive over time and symmetric.
Problem
You want to compute log returns between consecutive price observations.
Solution
Use LAG() to access the previous row's value, then compute the natural log of the price ratio.
Tick-by-tick log returns
This example computes log returns from the mid-price of individual quotes:
DECLARE
@symbol := 'EURUSD',
@lookback := '$now - 1m..$now'
SELECT
timestamp,
symbol,
round((bid_price + ask_price) / 2, 5) AS mid_price,
LN(
(bid_price + ask_price) / 2 /
LAG((bid_price + ask_price) / 2)
OVER (PARTITION BY symbol ORDER BY timestamp)
) AS log_return
FROM core_price
WHERE symbol = @symbol
AND ecn = 'LMAX'
AND timestamp IN @lookback;
The core_price table contains quotes from multiple ECNs (LMAX, EBS, Currenex, Hotspot). Filtering to a single ECN avoids mixing venue-level price differences into returns - without the filter, consecutive rows may come from different venues, producing spurious returns that reflect venue spreads rather than true price moves.
Raw tick-level log returns overstate volatility due to bid-ask bounce: even if the true price is unchanged, alternating bid- and ask-side quotes produce non-zero returns. For volatility estimation, use the fixed-frequency approach below, which aggregates ticks into bars before computing returns.
Fixed-frequency log returns
For returns at a fixed frequency, sample first then compute returns on the sampled result:
DECLARE
@symbol := 'EURUSD',
@lookback := '$now - 1d..$now'
WITH sampled AS (
SELECT
timestamp,
symbol,
last((bid_price + ask_price) / 2) AS close_mid
FROM core_price
WHERE symbol = @symbol
AND timestamp IN @lookback
SAMPLE BY 1m ALIGN TO CALENDAR
)
SELECT
timestamp,
symbol,
round(close_mid, 5) AS close_mid,
LN(close_mid / LAG(close_mid)
OVER (PARTITION BY symbol ORDER BY timestamp))
AS log_return
FROM sampled;
How it works
Log returns are defined as ln(P_t / P_{t-1}) where P_t is the current price and P_{t-1} is the previous price. They are preferred over simple returns because:
- Additive over time: you can sum log returns across periods to get the total return
- Symmetric: a round-trip always sums to zero. If the price goes from 100 to 110 and back to 100, the log returns are
ln(110/100) + ln(100/110) = 0. With simple returns the same round-trip gives asymmetric percentages (+10% up, -9.09% down)
LAG(value) OVER (ORDER BY timestamp) gives the previous row's value in timestamp order. The first row returns NULL since there is no predecessor, which propagates through LN as NULL.
When computing returns at a fixed frequency, always aggregate into bars first with SAMPLE BY in a CTE, then apply LAG() to the sampled result. This ensures the window function runs over the small number of bars (e.g. 1,440 for one-minute bars over a day) rather than millions of raw ticks. See this blog post for a detailed walkthrough of why the order matters.
- Realized Volatility - uses log returns to compute rolling volatility
- Cumulative Product - running product of returns
- Rolling Std Dev - rolling standard deviation of prices
- Window functions
- ln() function