Returns and risk
Returns, drawdown, volatility, z-score and the risk numbers of an investment: Sharpe, Sortino, Calmar, information ratio, value at risk and expected shortfall.
- Category: Functions — statistics
- Keywords: return, returns, daily return, monthly return, simple return, simple return, log return, logarithmic return, continuous return, cumulative return, total return, growth, drawdown, peak, MDD, maximum drawdown, volatility, annualized volatility, historical volatility, risk, risk-free rate, z-score, standard score, standardize, normalize, min-max scaling, sharpe, Sharpe ratio, sortino, Sortino ratio, downside deviation, calmar, Calmar ratio, information ratio, tracking error, benchmark, VaR, value at risk, CVaR, conditional value at risk, expected shortfall, portfolio, mutual fund, NAV, periods per year, annualize, 240 trading days
- See also: Window and series functions (pct-change), Statistics functions, Correlation and regression, Finance and price functions (cagr), Technical indicators, Window and series functions (cummax)
A return is how much an investment grew: a price that goes from 500 to 510 has a return of 0.02 (2%). Risk is how much the returns jump up and down, and how deep the losses can be. The functions on this page measure both.
There are two kinds:
- Series functions give a value for every row:
pct_return,log_return,cum_return,drawdown,volatility,zscore,minmax_scale. They follow the rules of the technical indicators:- Without
OVER, all result rows are one series in time order. With many symbols, always writeOVER (PARTITION BY symbol ORDER BY time). WHEREruns first: the function only sees the rows thatWHEREkeeps.- They cannot be in the same
SELECTasGROUP BY,SAMPLE BYor an aggregate.
- Without
- Aggregates give one value per group:
max_drawdown,sharpe_ratio,sortino_ratio,calmar_ratio,information_ratio,value_at_risk,expected_shortfall. Likeavg, they work withGROUP BY, skip NULL values, and giveNULLfor no rows. Most of them read returns, not prices. So the usual query has two steps: the returns in a subquery, the aggregate outside.
More rules:
- Returns are fractions:
0.025means +2.5%. Multiply by 100 for percent. (The older pct_change already gives percent:2.5.) - Periods per year: some functions take
periods, the number of returns in one year. They use it to give a yearly (annualized) number. Use240for daily returns of a market with about 240 trading days a year (252is usual for US markets),52for weekly and12for monthly returns. Without it the number is "per period" (per day, per month). - Warm-up: at the start of a series there are not enough rows yet, and the result is
NULL:
| Function | First value on row | NULL rows at the start |
|---|---|---|
pct_return(x), log_return(x) | 2 | 1 |
pct_return(x, k), log_return(x, k) | k + 1 | k |
cum_return(x), drawdown(x) | 1 | 0 |
volatility(price, n) | n + 1 | n |
zscore(x, n), minmax_scale(x, n) | n | n - 1 |
- Data of the examples: the daily candles (
ochl_day) have a gap between 2025-12-31 and 2026-08-23, so the daily examples usetime >= '2026-08-23'.fund_navholds the month-start NAV (the price of one unit) of two made-up mutual funds, GROWTH and INCOME, from 2024-09 to 2026-08.
Example: VOLT: daily return, total return and drawdown (sample data: sample, analysis-market)
SELECT time, close,
round(pct_return(close), 4) AS daily_return,
round(cum_return(close), 4) AS total_return,
round(drawdown(close), 4) AS drawdown
FROM ochl_day
WHERE symbol = 'VOLT' AND time >= '2026-08-23'
ORDER BY time;
Result:
| time (TIMESTAMP) | close (DOUBLE) | daily_return (DOUBLE) | total_return (DOUBLE) | drawdown (DOUBLE) |
|---|---|---|---|---|
| 2026-08-23T00:00:00.000Z | 181 | NULL | 0 | 0 |
| 2026-08-24T00:00:00.000Z | 178.5 | -0.0138 | -0.0138 | -0.0138 |
| 2026-08-25T00:00:00.000Z | 176.8 | -0.0095 | -0.0232 | -0.0232 |
| 2026-08-26T00:00:00.000Z | 174.2 | -0.0147 | -0.0376 | -0.0376 |
| 2026-08-27T00:00:00.000Z | 173.3 | -0.0052 | -0.0425 | -0.0425 |
| 2026-08-30T00:00:00.000Z | 173 | -0.0017 | -0.0442 | -0.0442 |
| 2026-08-31T00:00:00.000Z | 172.6 | -0.0023 | -0.0464 | -0.0464 |
| 2026-09-01T00:00:00.000Z | 169 | -0.0209 | -0.0663 | -0.0663 |
| 2026-09-02T00:00:00.000Z | 171.5 | 0.0148 | -0.0525 | -0.0525 |
| 2026-09-03T00:00:00.000Z | 176 | 0.0262 | -0.0276 | -0.0276 |
| 2026-09-06T00:00:00.000Z | 180.1 | 0.0233 | -0.005 | -0.005 |
| 2026-09-07T00:00:00.000Z | 176.9 | -0.0178 | -0.0227 | -0.0227 |
| 2026-09-08T00:00:00.000Z | 182.4 | 0.0311 | 0.0077 | 0 |
| 2026-09-09T00:00:00.000Z | 187.3 | 0.0269 | 0.0348 | 0 |
| 2026-09-10T00:00:00.000Z | 191.2 | 0.0208 | 0.0564 | 0 |
(15 rows)
Example: Risk report of two funds: monthly returns inside, the risk numbers outside (sample data: sample, analysis-market)
SELECT fund,
count(r) AS months,
round(avg(r) * 100, 2) AS avg_month_pct,
round(sharpe_ratio(r, 0, 12), 2) AS sharpe,
round(sortino_ratio(r, 0, 12), 2) AS sortino,
round(calmar_ratio(r, 12), 2) AS calmar,
round(max_drawdown(nav) * 100, 2) AS max_drawdown_pct,
round(value_at_risk(r, 0.95) * 100, 2) AS var95_pct
FROM (SELECT time, fund, nav,
pct_return(nav) OVER (PARTITION BY fund ORDER BY time) AS r
FROM fund_nav) t
GROUP BY fund
ORDER BY fund;
Result:
| fund (SYMBOL) | months (LONG) | avg_month_pct (DOUBLE) | sharpe (DOUBLE) | sortino (DOUBLE) | calmar (DOUBLE) | max_drawdown_pct (DOUBLE) | var95_pct (DOUBLE) |
|---|---|---|---|---|---|---|---|
| GROWTH | 23 | 0.89 | 1.35 | 2.33 | 1.26 | -8.61 | 3.09 |
| INCOME | 23 | 0.33 | 2.85 | 6.76 | 4.5 | -0.89 | 0.39 |
(2 rows)
pct_return, percent_return, returns
Syntax:
pct_return(x)
pct_return(x, k)
pct_return(x, k) OVER ([PARTITION BY ...] [ORDER BY ...])
percent_return(x [, k])
returns(x [, k])
The simple return: the change from k rows before, as a fraction: x / previous x - 1. On daily closes it is the daily return; 0.012 means the price rose 1.2%. percent_return and returns are the same function.
| Argument | Type | Meaning |
|---|---|---|
x | number | the price, usually close or a fund's NAV |
k | whole number, 1 or more | how many rows back; default 1. 5 on daily candles is the return over 5 trading days (about one week) |
- Returns:
DOUBLE, a fraction (0.012= +1.2%). - NULL:
NULLfor the firstkrows, when this or the earlier value isNULL, and when the earlier value is0. - Notes: the same as
pct_change(x) / 100, butpct_returncan lookkrows back.ROWSinsideOVERis ignored.
Example: Daily return of each symbol: a fraction, and pct_change in percent (sample data: sample, analysis-market)
SELECT time, symbol, close,
round(pct_return(close) OVER (PARTITION BY symbol ORDER BY time), 5) AS daily_return,
round(pct_change(close) OVER (PARTITION BY symbol ORDER BY time), 3) AS change_pct
FROM ochl_day
WHERE time >= '2026-09-08'
ORDER BY symbol, time;
Result:
| time (TIMESTAMP) | symbol (SYMBOL) | close (DOUBLE) | daily_return (DOUBLE) | change_pct (DOUBLE) |
|---|---|---|---|---|
| 2026-09-08T00:00:00.000Z | CITY | 497.5 | NULL | NULL |
| 2026-09-09T00:00:00.000Z | CITY | 504.6 | 0.01427 | 1.427 |
| 2026-09-10T00:00:00.000Z | CITY | 504.9 | 0.00059 | 0.059 |
| 2026-09-08T00:00:00.000Z | COAST | 377.2 | NULL | NULL |
| 2026-09-09T00:00:00.000Z | COAST | 380.1 | 0.00769 | 0.769 |
| 2026-09-10T00:00:00.000Z | COAST | 376.1 | -0.01052 | -1.052 |
| 2026-09-08T00:00:00.000Z | VOLT | 182.4 | NULL | NULL |
| 2026-09-09T00:00:00.000Z | VOLT | 187.3 | 0.02686 | 2.686 |
| 2026-09-10T00:00:00.000Z | VOLT | 191.2 | 0.02082 | 2.082 |
(9 rows)
Example: Return over the last 5 trading days (sample data: sample, analysis-market)
SELECT time, close, lag(close, 5) AS close_5_days_ago,
round(pct_return(close, 5) * 100, 2) AS return_5d_pct
FROM ochl_day
WHERE symbol = 'COAST' AND time >= '2026-08-23'
ORDER BY time;
Result:
| time (TIMESTAMP) | close (DOUBLE) | close_5_days_ago (DOUBLE) | return_5d_pct (DOUBLE) |
|---|---|---|---|
| 2026-08-23T00:00:00.000Z | 391 | NULL | NULL |
| 2026-08-24T00:00:00.000Z | 384.9 | NULL | NULL |
| 2026-08-25T00:00:00.000Z | 379.8 | NULL | NULL |
| 2026-08-26T00:00:00.000Z | 373 | NULL | NULL |
| 2026-08-27T00:00:00.000Z | 367.1 | NULL | NULL |
| 2026-08-30T00:00:00.000Z | 372.4 | 391 | -4.76 |
| 2026-08-31T00:00:00.000Z | 368.2 | 384.9 | -4.34 |
| 2026-09-01T00:00:00.000Z | 367.4 | 379.8 | -3.26 |
| 2026-09-02T00:00:00.000Z | 369.2 | 373 | -1.02 |
| 2026-09-03T00:00:00.000Z | 373.2 | 367.1 | 1.66 |
| 2026-09-06T00:00:00.000Z | 375.4 | 372.4 | 0.81 |
| 2026-09-07T00:00:00.000Z | 379.2 | 368.2 | 2.99 |
| 2026-09-08T00:00:00.000Z | 377.2 | 367.4 | 2.67 |
| 2026-09-09T00:00:00.000Z | 380.1 | 369.2 | 2.95 |
| 2026-09-10T00:00:00.000Z | 376.1 | 373.2 | 0.78 |
(15 rows)
Example: NULL and zero prices (sample data: sample, analysis-market)
Setup:
CREATE TABLE prices (time TIMESTAMP, close DOUBLE) TIMESERIES(time) PARTITION BY DAY;
INSERT INTO prices VALUES ('2026-09-01', 100), ('2026-09-02', 110), ('2026-09-03', NULL),
('2026-09-04', 99), ('2026-09-05', 121), ('2026-09-06', 0), ('2026-09-07', 50);
SELECT time, close,
round(pct_return(close), 4) AS pct_return,
round(log_return(close), 4) AS log_return,
round(cum_return(close), 4) AS cum_return,
round(drawdown(close), 4) AS drawdown
FROM prices
ORDER BY time;
Result:
| time (TIMESTAMP) | close (DOUBLE) | pct_return (DOUBLE) | log_return (DOUBLE) | cum_return (DOUBLE) | drawdown (DOUBLE) |
|---|---|---|---|---|---|
| 2026-09-01T00:00:00.000Z | 100 | NULL | NULL | 0 | 0 |
| 2026-09-02T00:00:00.000Z | 110 | 0.1 | 0.0953 | 0.1 | 0 |
| 2026-09-03T00:00:00.000Z | NULL | NULL | NULL | NULL | NULL |
| 2026-09-04T00:00:00.000Z | 99 | NULL | NULL | -0.01 | -0.1 |
| 2026-09-05T00:00:00.000Z | 121 | 0.2222 | 0.2007 | 0.21 | 0 |
| 2026-09-06T00:00:00.000Z | 0 | -1 | NULL | -1 | -1 |
| 2026-09-07T00:00:00.000Z | 50 | NULL | NULL | -0.5 | -0.5868 |
(7 rows)
log_return
Syntax:
log_return(x)
log_return(x, k)
log_return(x, k) OVER ([PARTITION BY ...] [ORDER BY ...])
The log return: ln(x / previous x). For small moves it is almost the same as the simple return: a rise from 507.9 to 510.3 is 0.00473 as a simple return and 0.00471 as a log return. Traders like it because log returns add up: the sum of the daily log returns is the log return of the whole period.
| Argument | Type | Meaning |
|---|---|---|
x | number | the price |
k | whole number, 1 or more | how many rows back; default 1 |
- Returns:
DOUBLE. - NULL:
NULLfor the firstkrows, when this or the earlier value isNULL, and when the ratio is0or less (a price of0). - Notes:
exp(log return) - 1is the simple return. volatility is made from log returns.
Example: Log returns add up to the log return of the whole period (sample data: sample, analysis-market)
SELECT symbol, first(close) AS first_close, last(close) AS last_close,
round(sum(lr), 6) AS sum_of_log_returns,
round(ln(last(close) / first(close)), 6) AS log_of_whole_period,
round(exp(sum(lr)) - 1, 6) AS total_return
FROM (SELECT time, symbol, close,
log_return(close) OVER (PARTITION BY symbol ORDER BY time) AS lr
FROM ochl_day
WHERE time >= '2026-08-23') t
GROUP BY symbol
ORDER BY symbol;
Result:
| symbol (SYMBOL) | first_close (DOUBLE) | last_close (DOUBLE) | sum_of_log_returns (DOUBLE) | log_of_whole_period (DOUBLE) | total_return (DOUBLE) |
|---|---|---|---|---|---|
| CITY | 507.9 | 504.9 | -0.005924 | -0.005924 | -0.005907 |
| COAST | 391 | 376.1 | -0.038852 | -0.038852 | -0.038107 |
| VOLT | 181 | 191.2 | 0.054823 | 0.054823 | 0.056354 |
(3 rows)
Example: Simple and log return of CITY's last 4 minutes (sample data: sample, analysis-market)
SELECT time, close, simple_return, log_return
FROM (SELECT time, close,
round(pct_return(close), 6) AS simple_return,
round(log_return(close), 6) AS log_return
FROM ochl_minutes
WHERE symbol = 'CITY') t
ORDER BY time DESC
LIMIT 4;
Result:
| time (TIMESTAMP) | close (DOUBLE) | simple_return (DOUBLE) | log_return (DOUBLE) |
|---|---|---|---|
| 2026-09-10T09:14:00.000Z | 504.9 | -0.000396 | -0.000396 |
| 2026-09-10T09:13:00.000Z | 505.1 | 0.000594 | 0.000594 |
| 2026-09-10T09:12:00.000Z | 504.8 | 0.000595 | 0.000594 |
| 2026-09-10T09:11:00.000Z | 504.5 | -0.000198 | -0.000198 |
(4 rows)
cum_return
Syntax:
cum_return(x)
cum_return(x) OVER ([PARTITION BY ...] [ORDER BY ...])
The cumulative (total) return: the change from the first value of the series to this row, x / first x - 1. It answers "how much did I make since I bought on the first day?".
| Argument | Type | Meaning |
|---|---|---|
x | number | the price or NAV |
- Returns:
DOUBLE, a fraction. The first row is0. - NULL:
NULLon rows wherexisNULL(the first value that is not NULL is the start).NULLon every row when the first value is0. - Notes: it takes prices, not returns.
100 × (1 + cum_return)is the value of 100 dollars invested at the start.
Example: What became of 100 dollars in each fund (the last 4 months) (sample data: sample, analysis-market)
SELECT time, fund, nav, growth_of_100
FROM (SELECT time, fund, nav,
round(100 * (1 + cum_return(nav) OVER (PARTITION BY fund ORDER BY time)), 2) AS growth_of_100
FROM fund_nav) t
WHERE time >= '2026-05-01'
ORDER BY fund, time;
Result:
| time (TIMESTAMP) | fund (SYMBOL) | nav (DOUBLE) | growth_of_100 (DOUBLE) |
|---|---|---|---|
| 2026-05-01T00:00:00.000Z | GROWTH | 11.31 | 113.1 |
| 2026-06-01T00:00:00.000Z | GROWTH | 11.66 | 116.6 |
| 2026-07-01T00:00:00.000Z | GROWTH | 11.94 | 119.4 |
| 2026-08-01T00:00:00.000Z | GROWTH | 12.18 | 121.8 |
| 2026-05-01T00:00:00.000Z | INCOME | 10.58 | 105.8 |
| 2026-06-01T00:00:00.000Z | INCOME | 10.65 | 106.5 |
| 2026-07-01T00:00:00.000Z | INCOME | 10.71 | 107.1 |
| 2026-08-01T00:00:00.000Z | INCOME | 10.78 | 107.8 |
(8 rows)
Example: Total return of every symbol since 23 August (sample data: sample, analysis-market)
SELECT symbol, close, total_return_pct
FROM (SELECT time, symbol, close,
round(cum_return(close) OVER (PARTITION BY symbol ORDER BY time) * 100, 2) AS total_return_pct
FROM ochl_day
WHERE time >= '2026-08-23') t
WHERE time IN '2026-09-10'
ORDER BY total_return_pct DESC;
Result:
| symbol (SYMBOL) | close (DOUBLE) | total_return_pct (DOUBLE) |
|---|---|---|
| VOLT | 191.2 | 5.64 |
| CITY | 504.9 | -0.59 |
| COAST | 376.1 | -3.81 |
(3 rows)
drawdown
Syntax:
drawdown(x)
drawdown(x) OVER ([PARTITION BY ...] [ORDER BY ...])
The drawdown: how far the price is below its highest value so far (the peak), as a fraction: x / peak - 1. -0.05 means 5% below the peak. It is 0 on a new high. Traders use it to see how painful a fall was, and when the price got back to its old high.
| Argument | Type | Meaning |
|---|---|---|
x | number | the price or NAV |
- Returns:
DOUBLE,0or less. - NULL:
NULLon rows wherexisNULL(NULL values do not change the peak), and while the peak is0or less. - Notes: the peak is the running maximum, the same as cummax. The deepest drawdown of a group is the aggregate max_drawdown.
Example: VOLT: the peak so far and the drawdown from it (sample data: sample, analysis-market)
SELECT time, close, cummax(close) AS peak, round(drawdown(close) * 100, 2) AS drawdown_pct
FROM ochl_day
WHERE symbol = 'VOLT' AND time >= '2026-08-23'
ORDER BY time;
Result:
| time (TIMESTAMP) | close (DOUBLE) | peak (DOUBLE) | drawdown_pct (DOUBLE) |
|---|---|---|---|
| 2026-08-23T00:00:00.000Z | 181 | 181 | 0 |
| 2026-08-24T00:00:00.000Z | 178.5 | 181 | -1.38 |
| 2026-08-25T00:00:00.000Z | 176.8 | 181 | -2.32 |
| 2026-08-26T00:00:00.000Z | 174.2 | 181 | -3.76 |
| 2026-08-27T00:00:00.000Z | 173.3 | 181 | -4.25 |
| 2026-08-30T00:00:00.000Z | 173 | 181 | -4.42 |
| 2026-08-31T00:00:00.000Z | 172.6 | 181 | -4.64 |
| 2026-09-01T00:00:00.000Z | 169 | 181 | -6.63 |
| 2026-09-02T00:00:00.000Z | 171.5 | 181 | -5.25 |
| 2026-09-03T00:00:00.000Z | 176 | 181 | -2.76 |
| 2026-09-06T00:00:00.000Z | 180.1 | 181 | -0.5 |
| 2026-09-07T00:00:00.000Z | 176.9 | 181 | -2.27 |
| 2026-09-08T00:00:00.000Z | 182.4 | 182.4 | 0 |
| 2026-09-09T00:00:00.000Z | 187.3 | 187.3 | 0 |
| 2026-09-10T00:00:00.000Z | 191.2 | 191.2 | 0 |
(15 rows)
Example: Days when a share was more than 3% below its peak (sample data: sample, analysis-market)
SELECT time, symbol, close, drawdown_pct
FROM (SELECT time, symbol, close,
round(drawdown(close) OVER (PARTITION BY symbol ORDER BY time) * 100, 2) AS drawdown_pct
FROM ochl_day
WHERE time >= '2026-08-23') t
WHERE drawdown_pct < -3
ORDER BY symbol, time;
Result:
| time (TIMESTAMP) | symbol (SYMBOL) | close (DOUBLE) | drawdown_pct (DOUBLE) |
|---|---|---|---|
| 2026-09-02T00:00:00.000Z | CITY | 496.4 | -3.29 |
| 2026-09-08T00:00:00.000Z | CITY | 497.5 | -3.08 |
| 2026-08-26T00:00:00.000Z | COAST | 373 | -4.6 |
| 2026-08-27T00:00:00.000Z | COAST | 367.1 | -6.11 |
| 2026-08-30T00:00:00.000Z | COAST | 372.4 | -4.76 |
| 2026-08-31T00:00:00.000Z | COAST | 368.2 | -5.83 |
| 2026-09-01T00:00:00.000Z | COAST | 367.4 | -6.04 |
| 2026-09-02T00:00:00.000Z | COAST | 369.2 | -5.58 |
| 2026-09-03T00:00:00.000Z | COAST | 373.2 | -4.55 |
| 2026-09-06T00:00:00.000Z | COAST | 375.4 | -3.99 |
| 2026-09-07T00:00:00.000Z | COAST | 379.2 | -3.02 |
| 2026-09-08T00:00:00.000Z | COAST | 377.2 | -3.53 |
| 2026-09-10T00:00:00.000Z | COAST | 376.1 | -3.81 |
| 2026-08-26T00:00:00.000Z | VOLT | 174.2 | -3.76 |
| 2026-08-27T00:00:00.000Z | VOLT | 173.3 | -4.25 |
| 2026-08-30T00:00:00.000Z | VOLT | 173 | -4.42 |
| 2026-08-31T00:00:00.000Z | VOLT | 172.6 | -4.64 |
| 2026-09-01T00:00:00.000Z | VOLT | 169 | -6.63 |
| 2026-09-02T00:00:00.000Z | VOLT | 171.5 | -5.25 |
(19 rows)
volatility
Syntax:
volatility(price, n)
volatility(price, n, periods)
volatility(price, n, periods) OVER ([PARTITION BY ...] [ORDER BY ...])
The historical volatility: how much the price jumps, as a fraction. It is the sample standard deviation of the last n log returns. With periods it is made yearly: × sqrt(periods). A yearly volatility of 0.25 means the price usually moves about 25% up or down in a year. High volatility means high risk.
| Argument | Type | Meaning |
|---|---|---|
price | number | the price, usually close |
n | whole number, 2 or more | how many returns to use (20 is about one month of trading days) |
periods | number, more than 0 | returns per year, to make it yearly: 240 for daily candles; default 1 (per period, not yearly) |
- Returns:
DOUBLE, a fraction. - NULL:
NULLfor the firstnrows (the first row has no return).NULLwhen one of thenreturns is missing (aNULLprice, or a price of0or less). - Notes: it uses log returns and divides by
n - 1, like most charting programs. For small daily moves it is almost the same as stddev of the lastnsimple returns.ROWSinsideOVERis ignored.
Example: CITY: 5-day volatility per day, and made yearly with 240 trading days (sample data: sample, analysis-market)
SELECT time, close,
round(volatility(close, 5), 5) AS vol_per_day,
round(volatility(close, 5, 240) * 100, 1) AS vol_per_year_pct
FROM ochl_day
WHERE symbol = 'CITY' AND time >= '2026-08-23'
ORDER BY time;
Result:
| time (TIMESTAMP) | close (DOUBLE) | vol_per_day (DOUBLE) | vol_per_year_pct (DOUBLE) |
|---|---|---|---|
| 2026-08-23T00:00:00.000Z | 507.9 | NULL | NULL |
| 2026-08-24T00:00:00.000Z | 510.3 | NULL | NULL |
| 2026-08-25T00:00:00.000Z | 513.3 | NULL | NULL |
| 2026-08-26T00:00:00.000Z | 513.2 | NULL | NULL |
| 2026-08-27T00:00:00.000Z | 505.9 | NULL | NULL |
| 2026-08-30T00:00:00.000Z | 503.1 | 0.00829 | 12.8 |
| 2026-08-31T00:00:00.000Z | 505.7 | 0.00838 | 13 |
| 2026-09-01T00:00:00.000Z | 502.4 | 0.0073 | 11.3 |
| 2026-09-02T00:00:00.000Z | 496.4 | 0.00755 | 11.7 |
| 2026-09-03T00:00:00.000Z | 497.9 | 0.00712 | 11 |
| 2026-09-06T00:00:00.000Z | 499 | 0.00732 | 11.3 |
| 2026-09-07T00:00:00.000Z | 498.7 | 0.00638 | 9.9 |
| 2026-09-08T00:00:00.000Z | 497.5 | 0.00603 | 9.3 |
| 2026-09-09T00:00:00.000Z | 504.6 | 0.00647 | 10 |
| 2026-09-10T00:00:00.000Z | 504.9 | 0.00658 | 10.2 |
(15 rows)
Example: Which share is most volatile? 10-day yearly volatility on 10 September (sample data: sample, analysis-market)
SELECT symbol, close, vol_year_pct
FROM (SELECT time, symbol, close,
round(volatility(close, 10, 240) OVER (PARTITION BY symbol ORDER BY time) * 100, 1) AS vol_year_pct
FROM ochl_day
WHERE time >= '2026-08-23') t
WHERE time IN '2026-09-10'
ORDER BY vol_year_pct DESC;
Result:
| symbol (SYMBOL) | close (DOUBLE) | vol_year_pct (DOUBLE) |
|---|---|---|
| VOLT | 191.2 | 29.6 |
| COAST | 376.1 | 14.2 |
| CITY | 504.9 | 11.1 |
(3 rows)
Example: n must be 2 or more (sample data: sample, analysis-market; shows an error)
SELECT volatility(close, 1) FROM ochl_day;
Result:
Error: line 1, column 8: volatility: the period must be a whole number of rows (2 or more)
zscore
Syntax:
zscore(x)
zscore(x) OVER ([PARTITION BY ...] [ORDER BY ...] [ROWS ...])
zscore(x, n)
zscore(x, n) OVER ([PARTITION BY ...] [ORDER BY ...])
The z-score (standard score): how many standard deviations x is above (+) or below (-) the average: (x - average) / stddev. 0 is a normal value; above 2 or below -2 is unusual. Traders use it to find strange volume, or a price far from its average ("mean reversion").
| Argument | Type | Meaning |
|---|---|---|
x | number | the value |
n | whole number, 2 or more | optional: compare with the last n rows (this row and the n - 1 before it) |
Which values it compares with:
| Call | Average and stddev of |
|---|---|
zscore(x) or zscore(x) OVER (PARTITION BY ...) | all rows of the series (or of the part) |
zscore(x) OVER (... ORDER BY time) | the first row to this row (running) |
zscore(x) OVER (... ORDER BY time ROWS 4 PRECEDING) | the frame: this row and the 4 before it |
zscore(x, n) | the last n rows; NULL until there are n rows |
- Returns:
DOUBLE. - NULL:
NULLwhenxisNULL, when there are fewer than 2 values, and when all values are equal (stddev0). Withoutn, NULL values in the window are skipped; withn, a NULL in the lastnrows givesNULL. - Notes: it uses the sample standard deviation (divide by
n - 1), like stddev.
Example: How unusual is the volume? z-score of each day's volume per symbol (sample data: sample, analysis-market)
SELECT time, symbol, volume, volume_z
FROM (SELECT time, symbol, volume,
round(zscore(volume) OVER (PARTITION BY symbol), 2) AS volume_z
FROM ochl_day
WHERE time >= '2026-08-23') t
WHERE time >= '2026-09-08'
ORDER BY symbol, time;
Result:
| time (TIMESTAMP) | symbol (SYMBOL) | volume (DOUBLE) | volume_z (DOUBLE) |
|---|---|---|---|
| 2026-09-08T00:00:00.000Z | CITY | 34400 | -2 |
| 2026-09-09T00:00:00.000Z | CITY | 182390 | 1.65 |
| 2026-09-10T00:00:00.000Z | CITY | 183040 | 1.67 |
| 2026-09-08T00:00:00.000Z | COAST | 60980 | -0.77 |
| 2026-09-09T00:00:00.000Z | COAST | 186330 | 1.85 |
| 2026-09-10T00:00:00.000Z | COAST | 186200 | 1.85 |
| 2026-09-08T00:00:00.000Z | VOLT | 136770 | 1.19 |
| 2026-09-09T00:00:00.000Z | VOLT | 94410 | 0.05 |
| 2026-09-10T00:00:00.000Z | VOLT | 41470 | -1.38 |
(9 rows)
Example: Rolling 5-day z-score against the running z-score (sample data: sample, analysis-market)
SELECT time, close,
round(zscore(close, 5), 3) AS z_last_5_days,
round(zscore(close) OVER (ORDER BY time), 3) AS z_since_start
FROM ochl_day
WHERE symbol = 'CITY' AND time >= '2026-08-23'
ORDER BY time;
Result:
| time (TIMESTAMP) | close (DOUBLE) | z_last_5_days (DOUBLE) | z_since_start (DOUBLE) |
|---|---|---|---|
| 2026-08-23T00:00:00.000Z | 507.9 | NULL | NULL |
| 2026-08-24T00:00:00.000Z | 510.3 | NULL | 0.707 |
| 2026-08-25T00:00:00.000Z | 513.3 | NULL | 1.035 |
| 2026-08-26T00:00:00.000Z | 513.2 | NULL | 0.782 |
| 2026-08-27T00:00:00.000Z | 505.9 | -1.297 | -1.297 |
| 2026-08-30T00:00:00.000Z | 503.1 | -1.338 | -1.432 |
| 2026-08-31T00:00:00.000Z | 505.7 | -0.54 | -0.71 |
| 2026-09-01T00:00:00.000Z | 502.4 | -0.855 | -1.261 |
| 2026-09-02T00:00:00.000Z | 496.4 | -1.638 | -1.842 |
| 2026-09-03T00:00:00.000Z | 497.9 | -0.832 | -1.325 |
| 2026-09-06T00:00:00.000Z | 499 | -0.341 | -1.024 |
| 2026-09-07T00:00:00.000Z | 498.7 | -0.081 | -0.982 |
| 2026-09-08T00:00:00.000Z | 497.5 | -0.388 | -1.082 |
| 2026-09-09T00:00:00.000Z | 504.6 | 1.75 | 0.106 |
| 2026-09-10T00:00:00.000Z | 504.9 | 1.124 | 0.153 |
(15 rows)
minmax_scale
Syntax:
minmax_scale(x)
minmax_scale(x) OVER ([PARTITION BY ...] [ORDER BY ...] [ROWS ...])
minmax_scale(x, n)
minmax_scale(x, n) OVER ([PARTITION BY ...] [ORDER BY ...])
Puts x on a scale from 0 to 1: (x - lowest) / (highest - lowest). 0 is the lowest value, 1 the highest, 0.5 the middle. Use it to draw shares with very different prices on one chart, or to see where today's close sits in its recent range.
| Argument | Type | Meaning |
|---|---|---|
x | number | the value |
n | whole number, 1 or more | optional: the lowest and highest of the last n rows |
The values it compares with are chosen like zscore: all rows, the window frame, or the last n rows.
- Returns:
DOUBLEfrom0to1. - NULL:
NULLwhenxisNULL, and when the lowest and highest are equal (for example the first row of a running window). Withn:NULLfor the firstn - 1rows, and when a value in the lastnrows isNULL. - Notes:
minmax_scale(close, 14)on a close is close to the fast stochastic, which uses the high and low of the candles instead.
Example: Three shares with different prices on one 0-1 scale (sample data: sample, analysis-market)
SELECT time, symbol, close, scaled
FROM (SELECT time, symbol, close,
round(minmax_scale(close) OVER (PARTITION BY symbol), 3) AS scaled
FROM ochl_day
WHERE time >= '2026-08-23') t
WHERE time >= '2026-09-08'
ORDER BY time, symbol;
Result:
| time (TIMESTAMP) | symbol (SYMBOL) | close (DOUBLE) | scaled (DOUBLE) |
|---|---|---|---|
| 2026-09-08T00:00:00.000Z | CITY | 497.5 | 0.065 |
| 2026-09-08T00:00:00.000Z | COAST | 377.2 | 0.423 |
| 2026-09-08T00:00:00.000Z | VOLT | 182.4 | 0.604 |
| 2026-09-09T00:00:00.000Z | CITY | 504.6 | 0.485 |
| 2026-09-09T00:00:00.000Z | COAST | 380.1 | 0.544 |
| 2026-09-09T00:00:00.000Z | VOLT | 187.3 | 0.824 |
| 2026-09-10T00:00:00.000Z | CITY | 504.9 | 0.503 |
| 2026-09-10T00:00:00.000Z | COAST | 376.1 | 0.377 |
| 2026-09-10T00:00:00.000Z | VOLT | 191.2 | 1 |
(9 rows)
Example: Where is the close in its 5-day range? (0 = at the low, 1 = at the high) (sample data: sample, analysis-market)
SELECT time, close, round(minmax_scale(close, 5), 3) AS place_in_5_day_range
FROM ochl_day
WHERE symbol = 'COAST' AND time >= '2026-08-23'
ORDER BY time;
Result:
| time (TIMESTAMP) | close (DOUBLE) | place_in_5_day_range (DOUBLE) |
|---|---|---|
| 2026-08-23T00:00:00.000Z | 391 | NULL |
| 2026-08-24T00:00:00.000Z | 384.9 | NULL |
| 2026-08-25T00:00:00.000Z | 379.8 | NULL |
| 2026-08-26T00:00:00.000Z | 373 | NULL |
| 2026-08-27T00:00:00.000Z | 367.1 | 0 |
| 2026-08-30T00:00:00.000Z | 372.4 | 0.298 |
| 2026-08-31T00:00:00.000Z | 368.2 | 0.087 |
| 2026-09-01T00:00:00.000Z | 367.4 | 0.051 |
| 2026-09-02T00:00:00.000Z | 369.2 | 0.396 |
| 2026-09-03T00:00:00.000Z | 373.2 | 1 |
| 2026-09-06T00:00:00.000Z | 375.4 | 1 |
| 2026-09-07T00:00:00.000Z | 379.2 | 1 |
| 2026-09-08T00:00:00.000Z | 377.2 | 0.8 |
| 2026-09-09T00:00:00.000Z | 380.1 | 1 |
| 2026-09-10T00:00:00.000Z | 376.1 | 0.149 |
(15 rows)
max_drawdown
Syntax:
max_drawdown(price)
max_drawdown(price) OVER (...)
The maximum drawdown: the biggest fall from a peak to a later low, as a fraction. -0.086 means that at the worst moment, an investor who bought at the top had lost 8.6%. It is the deepest value of drawdown. Fund reports show it next to the return.
| Argument | Type | Meaning |
|---|---|---|
price | number | the price or NAV (not returns) |
- Returns:
DOUBLE,0or less.0when the price never fell. - NULL: NULL prices are skipped.
NULLwhen there are no prices, or when a price is0or less. - Notes: an aggregate that needs time order: it reads the rows by their time (or in the order they come, when there is no time column). With
OVER (ORDER BY time)it gives the worst drawdown so far on every row.
Example: Worst fall of each fund (sample data: sample, analysis-market)
SELECT fund, first(nav) AS start_nav, last(nav) AS end_nav,
round(max_drawdown(nav) * 100, 2) AS max_drawdown_pct
FROM fund_nav
GROUP BY fund
ORDER BY fund;
Result:
| fund (SYMBOL) | start_nav (DOUBLE) | end_nav (DOUBLE) | max_drawdown_pct (DOUBLE) |
|---|---|---|---|
| GROWTH | 10 | 12.18 | -8.61 |
| INCOME | 10 | 10.78 | -0.89 |
(2 rows)
Example: Drawdown of the day and the worst drawdown so far (sample data: sample, analysis-market)
SELECT time, close,
round(drawdown(close) * 100, 2) AS drawdown_pct,
round(max_drawdown(close) OVER (ORDER BY time) * 100, 2) AS worst_so_far_pct
FROM ochl_day
WHERE symbol = 'VOLT' AND time >= '2026-08-23'
ORDER BY time;
Result:
| time (TIMESTAMP) | close (DOUBLE) | drawdown_pct (DOUBLE) | worst_so_far_pct (DOUBLE) |
|---|---|---|---|
| 2026-08-23T00:00:00.000Z | 181 | 0 | 0 |
| 2026-08-24T00:00:00.000Z | 178.5 | -1.38 | -1.38 |
| 2026-08-25T00:00:00.000Z | 176.8 | -2.32 | -2.32 |
| 2026-08-26T00:00:00.000Z | 174.2 | -3.76 | -3.76 |
| 2026-08-27T00:00:00.000Z | 173.3 | -4.25 | -4.25 |
| 2026-08-30T00:00:00.000Z | 173 | -4.42 | -4.42 |
| 2026-08-31T00:00:00.000Z | 172.6 | -4.64 | -4.64 |
| 2026-09-01T00:00:00.000Z | 169 | -6.63 | -6.63 |
| 2026-09-02T00:00:00.000Z | 171.5 | -5.25 | -6.63 |
| 2026-09-03T00:00:00.000Z | 176 | -2.76 | -6.63 |
| 2026-09-06T00:00:00.000Z | 180.1 | -0.5 | -6.63 |
| 2026-09-07T00:00:00.000Z | 176.9 | -2.27 | -6.63 |
| 2026-09-08T00:00:00.000Z | 182.4 | 0 | -6.63 |
| 2026-09-09T00:00:00.000Z | 187.3 | 0 | -6.63 |
| 2026-09-10T00:00:00.000Z | 191.2 | 0 | -6.63 |
(15 rows)
sharpe_ratio
Syntax:
sharpe_ratio(returns)
sharpe_ratio(returns, risk_free)
sharpe_ratio(returns, risk_free, periods)
The Sharpe ratio: how much return you get for each unit of risk. It is the average return above the risk-free rate, divided by the standard deviation of the returns: (avg(returns) - risk_free) / stddev(returns) × sqrt(periods). Bigger is better: above 1 (yearly) is good, above 2 very good. The risk-free rate is what a safe investment pays, like a bank fixed deposit.
| Argument | Type | Meaning |
|---|---|---|
returns | number | one return per row, as a fraction (from pct_return) |
risk_free | number | the safe return per period, as a fraction; default 0. A fixed deposit of 6% a year is 0.06 / 12 = 0.005 per month |
periods | number | returns per year, to make the ratio yearly (12 for monthly, 240 for daily stock returns); default 1 |
- Returns:
DOUBLE. - NULL: NULL returns are skipped.
NULLwith fewer than 2 returns, when all returns are equal, whenrisk_freeorperiodsisNULL, and whenperiodsis0or less. - Notes: it uses the sample standard deviation.
risk_freeandperiodsare read once, from the first row. A ratio made from a few returns (a few weeks of data) is not reliable.
Example: Sharpe ratio of each fund: per month, per year, and above a 6% fixed deposit (sample data: sample, analysis-market)
SELECT fund,
round(sharpe_ratio(r), 3) AS per_month,
round(sharpe_ratio(r, 0, 12), 3) AS per_year,
round(sharpe_ratio(r, 0.06 / 12, 12), 3) AS per_year_above_fd
FROM (SELECT time, fund, pct_return(nav) OVER (PARTITION BY fund ORDER BY time) AS r
FROM fund_nav) t
GROUP BY fund
ORDER BY fund;
Result:
| fund (SYMBOL) | per_month (DOUBLE) | per_year (DOUBLE) | per_year_above_fd (DOUBLE) |
|---|---|---|---|
| GROWTH | 0.39 | 1.351 | 0.588 |
| INCOME | 0.822 | 2.847 | -1.495 |
(2 rows)
Example: Yearly Sharpe ratio of the shares from daily returns (only 14 returns: just a demo) (sample data: sample, analysis-market)
SELECT symbol, count(r) AS returns, round(avg(r) * 100, 3) AS avg_day_pct,
round(sharpe_ratio(r, 0, 240), 2) AS sharpe_year
FROM (SELECT time, symbol, pct_return(close) OVER (PARTITION BY symbol ORDER BY time) AS r
FROM ochl_day
WHERE time >= '2026-08-23') t
GROUP BY symbol
ORDER BY sharpe_year DESC;
Result:
| symbol (SYMBOL) | returns (LONG) | avg_day_pct (DOUBLE) | sharpe_year (DOUBLE) |
|---|---|---|---|
| VOLT | 14 | 0.409 | 3.36 |
| CITY | 14 | -0.04 | -0.82 |
| COAST | 14 | -0.271 | -3.67 |
(3 rows)
sortino_ratio
Syntax:
sortino_ratio(returns)
sortino_ratio(returns, target)
sortino_ratio(returns, target, periods)
The Sortino ratio: like the Sharpe ratio, but it only counts the bad jumps as risk. Rises above the target do not hurt an investor, so only returns below target go into the risk (the downside deviation). (avg(returns) - target) / downside deviation × sqrt(periods). Bigger is better.
| Argument | Type | Meaning |
|---|---|---|
returns | number | one return per row, as a fraction |
target | number | the smallest return you are happy with, per period; default 0 |
periods | number | returns per year, to make it yearly; default 1 |
- Returns:
DOUBLE. - NULL: NULL returns are skipped.
NULLwhen no return is belowtarget(no downside), whentargetorperiodsisNULL, and whenperiodsis0or less. - Notes: the downside deviation is
sqrt(sum(min(0, return - target)²) / number of returns): it divides by all returns, like most fund reports. A fund with rare, small losses has a Sortino ratio much bigger than its Sharpe ratio.
Example: Sortino against Sharpe for both funds (sample data: sample, analysis-market)
SELECT fund,
round(sharpe_ratio(r, 0, 12), 2) AS sharpe,
round(sortino_ratio(r, 0, 12), 2) AS sortino,
round(sortino_ratio(r, 0.005, 12), 2) AS sortino_target_half_pct
FROM (SELECT time, fund, pct_return(nav) OVER (PARTITION BY fund ORDER BY time) AS r
FROM fund_nav) t
GROUP BY fund
ORDER BY fund;
Result:
| fund (SYMBOL) | sharpe (DOUBLE) | sortino (DOUBLE) | sortino_target_half_pct (DOUBLE) |
|---|---|---|---|
| GROWTH | 1.35 | 2.33 | 0.84 |
| INCOME | 2.85 | 6.76 | -1.43 |
(2 rows)
calmar_ratio
Syntax:
calmar_ratio(returns, periods)
The Calmar ratio: the yearly return divided by the maximum drawdown. It answers "how much did I earn per year for each 1% of the worst fall?". Bigger is better; above 1 is good. Fund reports usually use the last 36 monthly returns.
| Argument | Type | Meaning |
|---|---|---|
returns | number | one return per row, as a fraction, in time order |
periods | number | returns per year: 12 for monthly, 240 for daily stock returns (needed) |
- Returns:
DOUBLE. - NULL: NULL returns are skipped.
NULLwhen the value never fell (no drawdown), whenperiodsisNULL,0or less, and when the investment lost everything. - Notes: the yearly return is compounded, like cagr:
(product of (1 + return)) ^ (periods / number of returns) - 1. The drawdown comes from the same returns, in time order. Both arguments are needed.
Example: Calmar ratio of each fund, with its drawdown (sample data: sample, analysis-market)
SELECT fund, count(r) AS months,
round(max_drawdown(nav) * 100, 2) AS max_drawdown_pct,
round(calmar_ratio(r, 12), 2) AS calmar
FROM (SELECT time, fund, nav, pct_return(nav) OVER (PARTITION BY fund ORDER BY time) AS r
FROM fund_nav) t
GROUP BY fund
ORDER BY fund;
Result:
| fund (SYMBOL) | months (LONG) | max_drawdown_pct (DOUBLE) | calmar (DOUBLE) |
|---|---|---|---|
| GROWTH | 23 | -8.61 | 1.26 |
| INCOME | 23 | -0.89 | 4.5 |
(2 rows)
Example: The number of periods per year is needed (sample data: sample, analysis-market; shows an error)
SELECT calmar_ratio(nav) FROM fund_nav;
Result:
Error: line 1, column 8: function 'calmar_ratio' takes 2 argument(s), got 1
information_ratio
Syntax:
information_ratio(returns, benchmark)
information_ratio(returns, benchmark, periods)
The information ratio: how well an investment beats a benchmark (a market index), for the risk of being different from it. It is the average of returns - benchmark, divided by the standard deviation of that difference (the tracking error), × sqrt(periods). Above 0 the share did better than the index; above 0.5 (yearly) is good.
| Argument | Type | Meaning |
|---|---|---|
returns | number | the returns of the share or fund |
benchmark | number | the returns of the index on the same days |
periods | number | returns per year, to make it yearly; default 1 |
- Returns:
DOUBLE. - NULL: a row where
returnsorbenchmarkisNULLis skipped.NULLwith fewer than 2 rows, when the difference is always the same (tracking error0), and whenperiodsisNULL,0or less. - Notes: join the share and the index on the time, and make both returns in the same subquery.
Example: Did the shares beat the market index? (daily returns since 23 August) (sample data: sample, analysis-market)
SELECT symbol, count(r) AS days,
round(avg(r - m) * 100, 3) AS avg_excess_day_pct,
round(information_ratio(r, m), 3) AS per_day,
round(information_ratio(r, m, 240), 2) AS per_year
FROM (SELECT d.time, d.symbol,
pct_return(d.close) OVER (PARTITION BY d.symbol ORDER BY d.time) AS r,
pct_return(i.close) OVER (PARTITION BY d.symbol ORDER BY d.time) AS m
FROM ochl_day d
JOIN market_index i ON i.time = d.time
WHERE d.time >= '2026-08-23') t
GROUP BY symbol
ORDER BY per_year DESC;
Result:
| symbol (SYMBOL) | days (LONG) | avg_excess_day_pct (DOUBLE) | per_day (DOUBLE) | per_year (DOUBLE) |
|---|---|---|---|---|
| VOLT | 14 | 0.41 | 0.252 | 3.9 |
| CITY | 14 | -0.038 | -0.058 | -0.89 |
| COAST | 14 | -0.27 | -0.294 | -4.55 |
(3 rows)
value_at_risk
Syntax:
value_at_risk(returns, confidence)
The value at risk (VaR): a loss that is not passed in most periods. With confidence 0.95, a VaR of 0.031 means: in 95 months of 100 the loss is at most 3.1%; in about 5 of 100 it is bigger. Banks and brokers use it to set limits: "the 1-day 95% VaR of this position is $12,000".
| Argument | Type | Meaning |
|---|---|---|
returns | number | one return per row, as a fraction |
confidence | number, more than 0 and less than 1 | how sure: 0.95 or 0.99 |
- Returns:
DOUBLE, the loss as a positive fraction. It is negative when even the bad returns are gains. - NULL: NULL returns are skipped.
NULLwith no returns, and whenconfidenceisNULL. - Notes: this is historical VaR: minus the
1 - confidencepercentile of the returns you have, with the same interpolation as percentile. It needs many returns to be useful. Multiply by the money invested to get an amount of money.confidenceis read once, from the first row.
Example: 95% monthly VaR of each fund, and in dollars for 500,000 invested (sample data: sample, analysis-market)
SELECT fund,
round(value_at_risk(r, 0.95) * 100, 2) AS var95_pct,
round(value_at_risk(r, 0.95) * 500000) AS var95_dollars
FROM (SELECT time, fund, pct_return(nav) OVER (PARTITION BY fund ORDER BY time) AS r
FROM fund_nav) t
GROUP BY fund
ORDER BY fund;
Result:
| fund (SYMBOL) | var95_pct (DOUBLE) | var95_dollars (DOUBLE) |
|---|---|---|
| GROWTH | 3.09 | 15433 |
| INCOME | 0.39 | 1927 |
(2 rows)
Example: Confidence is a fraction, not a percent (sample data: sample, analysis-market; shows an error)
SELECT value_at_risk(close, 95) FROM ochl_day;
Result:
Error: line 1, column 8: value_at_risk: the confidence level must be more than 0 and less than 1, like 0.95, got 95
expected_shortfall
Syntax:
expected_shortfall(returns, confidence)
The expected shortfall (also called CVaR, conditional value at risk): the average loss in the bad periods that value_at_risk leaves out. VaR says "in the worst 5% of months you lose at least this much"; expected shortfall says "and in those months you lose this much on average". It is always the same as VaR or bigger.
| Argument | Type | Meaning |
|---|---|---|
returns | number | one return per row, as a fraction |
confidence | number, more than 0 and less than 1 | how sure: 0.95 or 0.99 |
- Returns:
DOUBLE, the average loss as a positive fraction. - NULL: NULL returns are skipped.
NULLwith no returns, and whenconfidenceisNULL. - Notes: it is minus the average of the returns at or below the
1 - confidencepercentile.
Example: Both funds: VaR and expected shortfall of monthly returns, at 90% and 95% (sample data: sample, analysis-market)
SELECT fund,
round(value_at_risk(r, 0.90) * 100, 2) AS var90_pct,
round(expected_shortfall(r, 0.90) * 100, 2) AS es90_pct,
round(value_at_risk(r, 0.95) * 100, 2) AS var95_pct,
round(expected_shortfall(r, 0.95) * 100, 2) AS es95_pct
FROM (SELECT time, fund, pct_return(nav) OVER (PARTITION BY fund ORDER BY time) AS r
FROM fund_nav) t
GROUP BY fund
ORDER BY fund;
Result:
| fund (SYMBOL) | var90_pct (DOUBLE) | es90_pct (DOUBLE) | var95_pct (DOUBLE) | es95_pct (DOUBLE) |
|---|---|---|---|---|
| GROWTH | 2.31 | 3.08 | 3.09 | 3.45 |
| INCOME | 0.28 | 0.42 | 0.39 | 0.48 |
(2 rows)
Example: The worst monthly returns of GROWTH, to see where the numbers come from (sample data: sample, analysis-market)
SELECT time, round(r * 100, 2) AS return_pct
FROM (SELECT time, pct_return(nav) AS r FROM fund_nav WHERE fund = 'GROWTH') t
WHERE r < 0
ORDER BY r
LIMIT 4;
Result:
| time (TIMESTAMP) | return_pct (DOUBLE) |
|---|---|
| 2026-04-01T00:00:00.000Z | -3.73 |
| 2025-02-01T00:00:00.000Z | -3.17 |
| 2025-03-01T00:00:00.000Z | -2.35 |
| 2025-01-01T00:00:00.000Z | -2.13 |
(4 rows)