| 1 | SET search_path TO trekr;
|
|---|
| 2 |
|
|---|
| 3 | -- P6 Q4 (Annual): Investing diversification, concentration risk, and contribution pacing.
|
|---|
| 4 | -- Change report_year in params CTE when needed.
|
|---|
| 5 | WITH params AS (
|
|---|
| 6 | SELECT 2026::int AS report_year
|
|---|
| 7 | ),
|
|---|
| 8 | months AS (
|
|---|
| 9 | SELECT generate_series(1, 12) AS month_no
|
|---|
| 10 | ),
|
|---|
| 11 | investor_base AS (
|
|---|
| 12 | SELECT
|
|---|
| 13 | iu.user_id,
|
|---|
| 14 | u.username,
|
|---|
| 15 | u.email
|
|---|
| 16 | FROM investor_users iu
|
|---|
| 17 | JOIN users u ON u.user_id = iu.user_id
|
|---|
| 18 | ),
|
|---|
| 19 | annual_asset_lots AS (
|
|---|
| 20 | SELECT
|
|---|
| 21 | a.user_id,
|
|---|
| 22 | a.ticker_symbol,
|
|---|
| 23 | COALESCE(a.quantity, 0) AS quantity,
|
|---|
| 24 | COALESCE(a.buy_price, 0) AS buy_price,
|
|---|
| 25 | COALESCE(a.quantity, 0) * COALESCE(a.buy_price, 0) AS invested_amount,
|
|---|
| 26 | a.buy_date
|
|---|
| 27 | FROM assets a
|
|---|
| 28 | WHERE EXTRACT(YEAR FROM a.buy_date)::int = (SELECT report_year FROM params)
|
|---|
| 29 | ),
|
|---|
| 30 | ticker_rollup AS (
|
|---|
| 31 | SELECT
|
|---|
| 32 | aal.user_id,
|
|---|
| 33 | aal.ticker_symbol,
|
|---|
| 34 | SUM(aal.quantity) AS total_quantity,
|
|---|
| 35 | SUM(aal.invested_amount) AS total_invested_amount,
|
|---|
| 36 | COUNT(*) AS lot_count,
|
|---|
| 37 | MIN(aal.buy_date) AS first_buy_date,
|
|---|
| 38 | MAX(aal.buy_date) AS last_buy_date
|
|---|
| 39 | FROM annual_asset_lots aal
|
|---|
| 40 | GROUP BY aal.user_id, aal.ticker_symbol
|
|---|
| 41 | ),
|
|---|
| 42 | portfolio_totals AS (
|
|---|
| 43 | SELECT
|
|---|
| 44 | user_id,
|
|---|
| 45 | SUM(total_invested_amount) AS annual_total_invested,
|
|---|
| 46 | SUM(lot_count) AS annual_lot_count,
|
|---|
| 47 | COUNT(*) AS distinct_tickers
|
|---|
| 48 | FROM ticker_rollup
|
|---|
| 49 | GROUP BY user_id
|
|---|
| 50 | ),
|
|---|
| 51 | weights AS (
|
|---|
| 52 | SELECT
|
|---|
| 53 | tr.user_id,
|
|---|
| 54 | tr.ticker_symbol,
|
|---|
| 55 | tr.total_invested_amount,
|
|---|
| 56 | pt.annual_total_invested,
|
|---|
| 57 | (tr.total_invested_amount / NULLIF(pt.annual_total_invested, 0)) AS position_weight,
|
|---|
| 58 | DENSE_RANK() OVER (
|
|---|
| 59 | PARTITION BY tr.user_id
|
|---|
| 60 | ORDER BY tr.total_invested_amount DESC, tr.ticker_symbol ASC
|
|---|
| 61 | ) AS position_rank
|
|---|
| 62 | FROM ticker_rollup tr
|
|---|
| 63 | JOIN portfolio_totals pt ON pt.user_id = tr.user_id
|
|---|
| 64 | ),
|
|---|
| 65 | concentration AS (
|
|---|
| 66 | SELECT
|
|---|
| 67 | user_id,
|
|---|
| 68 | SUM(position_weight * position_weight) AS hhi_concentration,
|
|---|
| 69 | MAX(position_weight) AS top_position_weight,
|
|---|
| 70 | MAX(ticker_symbol) FILTER (WHERE position_rank = 1) AS top_ticker
|
|---|
| 71 | FROM weights
|
|---|
| 72 | GROUP BY user_id
|
|---|
| 73 | ),
|
|---|
| 74 | monthly_investment AS (
|
|---|
| 75 | SELECT
|
|---|
| 76 | ib.user_id,
|
|---|
| 77 | m.month_no,
|
|---|
| 78 | COALESCE(SUM(a.quantity * a.buy_price), 0) AS monthly_invested_amount
|
|---|
| 79 | FROM investor_base ib
|
|---|
| 80 | CROSS JOIN months m
|
|---|
| 81 | LEFT JOIN assets a
|
|---|
| 82 | ON a.user_id = ib.user_id
|
|---|
| 83 | AND EXTRACT(YEAR FROM a.buy_date)::int = (SELECT report_year FROM params)
|
|---|
| 84 | AND EXTRACT(MONTH FROM a.buy_date)::int = m.month_no
|
|---|
| 85 | GROUP BY ib.user_id, m.month_no
|
|---|
| 86 | ),
|
|---|
| 87 | monthly_investment_stats AS (
|
|---|
| 88 | SELECT
|
|---|
| 89 | user_id,
|
|---|
| 90 | AVG(monthly_invested_amount) AS avg_monthly_contribution,
|
|---|
| 91 | STDDEV_SAMP(monthly_invested_amount) AS contribution_stddev,
|
|---|
| 92 | COUNT(*) FILTER (WHERE monthly_invested_amount > 0) AS active_investing_months
|
|---|
| 93 | FROM monthly_investment
|
|---|
| 94 | GROUP BY user_id
|
|---|
| 95 | )
|
|---|
| 96 | SELECT
|
|---|
| 97 | ib.user_id,
|
|---|
| 98 | ib.username,
|
|---|
| 99 | ib.email,
|
|---|
| 100 | COALESCE(pt.annual_total_invested, 0) AS annual_total_invested,
|
|---|
| 101 | COALESCE(pt.annual_lot_count, 0) AS annual_lot_count,
|
|---|
| 102 | COALESCE(pt.distinct_tickers, 0) AS distinct_tickers,
|
|---|
| 103 | ROUND(COALESCE(ms.avg_monthly_contribution, 0)::numeric, 2) AS avg_monthly_contribution,
|
|---|
| 104 | COALESCE(ms.active_investing_months, 0) AS active_investing_months,
|
|---|
| 105 | ROUND((COALESCE(ms.active_investing_months, 0) / 12.0)::numeric, 4) AS activity_ratio,
|
|---|
| 106 | ROUND(COALESCE(c.hhi_concentration, 0)::numeric, 4) AS hhi_concentration,
|
|---|
| 107 | ROUND((1 - COALESCE(c.hhi_concentration, 1))::numeric, 4) AS diversification_index,
|
|---|
| 108 | ROUND(COALESCE(c.top_position_weight, 0)::numeric, 4) AS top_position_weight,
|
|---|
| 109 | c.top_ticker,
|
|---|
| 110 | ROUND((COALESCE(ms.contribution_stddev, 0) / NULLIF(ms.avg_monthly_contribution, 0))::numeric, 4) AS contribution_volatility_cv,
|
|---|
| 111 | DENSE_RANK() OVER (
|
|---|
| 112 | ORDER BY
|
|---|
| 113 | (1 - COALESCE(c.hhi_concentration, 1)) DESC,
|
|---|
| 114 | COALESCE(pt.annual_total_invested, 0) DESC,
|
|---|
| 115 | COALESCE(ms.active_investing_months, 0) DESC,
|
|---|
| 116 | ib.user_id ASC
|
|---|
| 117 | ) AS investing_annual_rank
|
|---|
| 118 | FROM investor_base ib
|
|---|
| 119 | LEFT JOIN portfolio_totals pt ON pt.user_id = ib.user_id
|
|---|
| 120 | LEFT JOIN concentration c ON c.user_id = ib.user_id
|
|---|
| 121 | LEFT JOIN monthly_investment_stats ms ON ms.user_id = ib.user_id
|
|---|
| 122 | ORDER BY investing_annual_rank, ib.user_id;
|
|---|
| 123 |
|
|---|