| 1 | = Напредни извештаи од базата (SQL, складирани процедури и релациона алгебра)
|
|---|
| 2 |
|
|---|
| 3 | === 1. Детален годишен извештај за финансиска резилиентност, стабилност на приходи и буџетски притисок по корисник
|
|---|
| 4 |
|
|---|
| 5 | ==== SQL
|
|---|
| 6 | {{{
|
|---|
| 7 | SET search_path TO trekr;
|
|---|
| 8 |
|
|---|
| 9 | WITH params AS (
|
|---|
| 10 | SELECT 2026::int AS report_year
|
|---|
| 11 | ),
|
|---|
| 12 | months AS (
|
|---|
| 13 | SELECT generate_series(1, 12) AS month_no
|
|---|
| 14 | ),
|
|---|
| 15 | finance_base AS (
|
|---|
| 16 | SELECT
|
|---|
| 17 | fu.user_id,
|
|---|
| 18 | u.username,
|
|---|
| 19 | u.email,
|
|---|
| 20 | COALESCE(fu.spending_budget, 0) AS spending_budget,
|
|---|
| 21 | COALESCE(fu.saving_budget, 0) AS saving_budget,
|
|---|
| 22 | COALESCE(fu.investing_budget, 0) AS investing_budget,
|
|---|
| 23 | COALESCE(fu.donation_budget, 0) AS donation_budget,
|
|---|
| 24 | COALESCE(fu.credit, 0) AS credit
|
|---|
| 25 | FROM finance_users fu
|
|---|
| 26 | JOIN users u ON u.user_id = fu.user_id
|
|---|
| 27 | ),
|
|---|
| 28 | monthly_income AS (
|
|---|
| 29 | SELECT
|
|---|
| 30 | fb.user_id,
|
|---|
| 31 | m.month_no,
|
|---|
| 32 | COALESCE(SUM(i.amount), 0) AS month_income
|
|---|
| 33 | FROM finance_base fb
|
|---|
| 34 | CROSS JOIN months m
|
|---|
| 35 | LEFT JOIN incomes i
|
|---|
| 36 | ON i.user_id = fb.user_id
|
|---|
| 37 | AND EXTRACT(YEAR FROM i.date)::int = (SELECT report_year FROM params)
|
|---|
| 38 | AND EXTRACT(MONTH FROM i.date)::int = m.month_no
|
|---|
| 39 | GROUP BY fb.user_id, m.month_no
|
|---|
| 40 | ),
|
|---|
| 41 | monthly_income_ranked AS (
|
|---|
| 42 | SELECT
|
|---|
| 43 | mi.*,
|
|---|
| 44 | DENSE_RANK() OVER (PARTITION BY mi.user_id ORDER BY mi.month_income DESC, mi.month_no ASC) AS best_month_rank,
|
|---|
| 45 | DENSE_RANK() OVER (PARTITION BY mi.user_id ORDER BY mi.month_income ASC, mi.month_no ASC) AS worst_month_rank
|
|---|
| 46 | FROM monthly_income mi
|
|---|
| 47 | ),
|
|---|
| 48 | annual_income AS (
|
|---|
| 49 | SELECT
|
|---|
| 50 | user_id,
|
|---|
| 51 | SUM(month_income) AS total_income,
|
|---|
| 52 | AVG(month_income) AS avg_monthly_income,
|
|---|
| 53 | STDDEV_SAMP(month_income) AS income_stddev,
|
|---|
| 54 | MAX(month_income) AS best_month_income,
|
|---|
| 55 | MIN(month_income) AS worst_month_income,
|
|---|
| 56 | COUNT(*) FILTER (WHERE month_income > 0) AS active_income_months
|
|---|
| 57 | FROM monthly_income
|
|---|
| 58 | GROUP BY user_id
|
|---|
| 59 | ),
|
|---|
| 60 | best_worst_months AS (
|
|---|
| 61 | SELECT
|
|---|
| 62 | user_id,
|
|---|
| 63 | MAX(month_no) FILTER (WHERE best_month_rank = 1) AS best_month_no,
|
|---|
| 64 | MAX(month_no) FILTER (WHERE worst_month_rank = 1) AS worst_month_no
|
|---|
| 65 | FROM monthly_income_ranked
|
|---|
| 66 | GROUP BY user_id
|
|---|
| 67 | )
|
|---|
| 68 | SELECT
|
|---|
| 69 | fb.user_id,
|
|---|
| 70 | fb.username,
|
|---|
| 71 | fb.email,
|
|---|
| 72 | (fb.spending_budget + fb.saving_budget + fb.investing_budget + fb.donation_budget) * 12 AS planned_annual_budget,
|
|---|
| 73 | ai.total_income AS actual_annual_income,
|
|---|
| 74 | ai.avg_monthly_income,
|
|---|
| 75 | ai.active_income_months,
|
|---|
| 76 | ai.best_month_income,
|
|---|
| 77 | ai.worst_month_income,
|
|---|
| 78 | bwm.best_month_no,
|
|---|
| 79 | bwm.worst_month_no,
|
|---|
| 80 | ROUND(
|
|---|
| 81 | (ai.income_stddev / NULLIF(ai.avg_monthly_income, 0))::numeric,
|
|---|
| 82 | 4
|
|---|
| 83 | ) AS income_volatility_cv,
|
|---|
| 84 | ROUND(
|
|---|
| 85 | (ai.total_income - (fb.spending_budget * 12))::numeric,
|
|---|
| 86 | 2
|
|---|
| 87 | ) AS annual_free_cash_after_spending,
|
|---|
| 88 | ROUND(
|
|---|
| 89 | ((fb.spending_budget * 12) / NULLIF(ai.total_income, 0))::numeric,
|
|---|
| 90 | 4
|
|---|
| 91 | ) AS spending_pressure_ratio,
|
|---|
| 92 | ROUND(
|
|---|
| 93 | (fb.credit / NULLIF(ai.total_income, 0))::numeric,
|
|---|
| 94 | 4
|
|---|
| 95 | ) AS leverage_ratio,
|
|---|
| 96 | DENSE_RANK() OVER (
|
|---|
| 97 | ORDER BY
|
|---|
| 98 | (ai.total_income - (fb.spending_budget * 12)) DESC,
|
|---|
| 99 | ((fb.spending_budget * 12) / NULLIF(ai.total_income, 0)) ASC,
|
|---|
| 100 | fb.user_id ASC
|
|---|
| 101 | ) AS finance_resilience_rank
|
|---|
| 102 | FROM finance_base fb
|
|---|
| 103 | JOIN annual_income ai ON ai.user_id = fb.user_id
|
|---|
| 104 | JOIN best_worst_months bwm ON bwm.user_id = fb.user_id
|
|---|
| 105 | ORDER BY finance_resilience_rank, fb.user_id;
|
|---|
| 106 | }}}
|
|---|
| 107 |
|
|---|
| 108 | ==== Релациона Алгебра
|
|---|
| 109 | {{{
|
|---|
| 110 | FB <- pi_{fu.user_id, u.username, u.email,
|
|---|
| 111 | COALESCE(fu.spending_budget,0)->spending_budget,
|
|---|
| 112 | COALESCE(fu.saving_budget,0)->saving_budget,
|
|---|
| 113 | COALESCE(fu.investing_budget,0)->investing_budget,
|
|---|
| 114 | COALESCE(fu.donation_budget,0)->donation_budget,
|
|---|
| 115 | COALESCE(fu.credit,0)->credit}
|
|---|
| 116 | (finance_users fu bowtie_{fu.user_id = u.user_id} users u)
|
|---|
| 117 |
|
|---|
| 118 | FBM <- FB x M
|
|---|
| 119 | IY <- sigma_{YEAR(i.date)=Y}(incomes i)
|
|---|
| 120 | MI0 <- FBM leftouterjoin_{FBM.user_id = i.user_id AND FBM.month_no = MONTH(i.date)} IY
|
|---|
| 121 | MI <- gamma_{user_id, month_no;
|
|---|
| 122 | SUM(COALESCE(i.amount,0))->month_income}(MI0)
|
|---|
| 123 |
|
|---|
| 124 | MIR <- omega_{PARTITION BY user_id ORDER BY month_income DESC, month_no ASC;
|
|---|
| 125 | DENSE_RANK()->best_month_rank,
|
|---|
| 126 | DENSE_RANK(PARTITION BY user_id ORDER BY month_income ASC, month_no ASC)->worst_month_rank}(MI)
|
|---|
| 127 |
|
|---|
| 128 | AI <- gamma_{user_id;
|
|---|
| 129 | SUM(month_income)->total_income,
|
|---|
| 130 | AVG(month_income)->avg_monthly_income,
|
|---|
| 131 | STDDEV_SAMP(month_income)->income_stddev,
|
|---|
| 132 | MAX(month_income)->best_month_income,
|
|---|
| 133 | MIN(month_income)->worst_month_income,
|
|---|
| 134 | COUNT_IF(month_income>0)->active_income_months}(MI)
|
|---|
| 135 |
|
|---|
| 136 | BWM <- gamma_{user_id;
|
|---|
| 137 | MAX_IF(month_no, best_month_rank=1)->best_month_no,
|
|---|
| 138 | MAX_IF(month_no, worst_month_rank=1)->worst_month_no}(MIR)
|
|---|
| 139 |
|
|---|
| 140 | R0 <- FB bowtie_{FB.user_id=AI.user_id} AI bowtie_{FB.user_id=BWM.user_id} BWM
|
|---|
| 141 | R1 <- alpha_{(spending_budget+saving_budget+investing_budget+donation_budget)*12->planned_annual_budget,
|
|---|
| 142 | total_income->actual_annual_income,
|
|---|
| 143 | income_stddev/NULLIF(avg_monthly_income,0)->income_volatility_cv,
|
|---|
| 144 | total_income-(spending_budget*12)->annual_free_cash_after_spending,
|
|---|
| 145 | (spending_budget*12)/NULLIF(total_income,0)->spending_pressure_ratio,
|
|---|
| 146 | credit/NULLIF(total_income,0)->leverage_ratio}(R0)
|
|---|
| 147 | R <- omega_{ORDER BY annual_free_cash_after_spending DESC,
|
|---|
| 148 | spending_pressure_ratio ASC,
|
|---|
| 149 | user_id ASC;
|
|---|
| 150 | DENSE_RANK()->finance_resilience_rank}(R1)
|
|---|
| 151 | }}}
|
|---|
| 152 |
|
|---|
| 153 | === 2. Детален годишен извештај за конзистентност на тренинг, оптоварување и тренд на перформанс
|
|---|
| 154 |
|
|---|
| 155 | ==== SQL
|
|---|
| 156 | {{{
|
|---|
| 157 | SET search_path TO trekr;
|
|---|
| 158 |
|
|---|
| 159 | WITH params AS (
|
|---|
| 160 | SELECT 2026::int AS report_year
|
|---|
| 161 | ),
|
|---|
| 162 | months AS (
|
|---|
| 163 | SELECT generate_series(1, 12) AS month_no
|
|---|
| 164 | ),
|
|---|
| 165 | training_base AS (
|
|---|
| 166 | SELECT
|
|---|
| 167 | tu.user_id,
|
|---|
| 168 | u.username,
|
|---|
| 169 | u.email,
|
|---|
| 170 | tu.gender,
|
|---|
| 171 | tu.age,
|
|---|
| 172 | tu.weight
|
|---|
| 173 | FROM training_users tu
|
|---|
| 174 | JOIN users u ON u.user_id = tu.user_id
|
|---|
| 175 | ),
|
|---|
| 176 | monthly_sessions AS (
|
|---|
| 177 | SELECT
|
|---|
| 178 | tb.user_id,
|
|---|
| 179 | m.month_no,
|
|---|
| 180 | COALESCE(COUNT(ts.training_id), 0) AS sessions_count,
|
|---|
| 181 | COALESCE(SUM(ts.duration), 0) AS total_duration_minutes,
|
|---|
| 182 | COALESCE(SUM(ts.calories), 0) AS total_calories,
|
|---|
| 183 | COALESCE(AVG(ts.duration), 0) AS avg_session_duration,
|
|---|
| 184 | COALESCE(AVG(ts.calories), 0) AS avg_session_calories
|
|---|
| 185 | FROM training_base tb
|
|---|
| 186 | CROSS JOIN months m
|
|---|
| 187 | LEFT JOIN training_sessions ts
|
|---|
| 188 | ON ts.training_user_id = tb.user_id
|
|---|
| 189 | AND EXTRACT(YEAR FROM ts.date)::int = (SELECT report_year FROM params)
|
|---|
| 190 | AND EXTRACT(MONTH FROM ts.date)::int = m.month_no
|
|---|
| 191 | GROUP BY tb.user_id, m.month_no
|
|---|
| 192 | ),
|
|---|
| 193 | monthly_ranked AS (
|
|---|
| 194 | SELECT
|
|---|
| 195 | ms.*,
|
|---|
| 196 | DENSE_RANK() OVER (PARTITION BY ms.user_id ORDER BY ms.total_calories DESC, ms.month_no ASC) AS peak_calorie_month_rank,
|
|---|
| 197 | DENSE_RANK() OVER (PARTITION BY ms.user_id ORDER BY ms.sessions_count DESC, ms.month_no ASC) AS peak_sessions_month_rank
|
|---|
| 198 | FROM monthly_sessions ms
|
|---|
| 199 | ),
|
|---|
| 200 | active_month_streaks AS (
|
|---|
| 201 | SELECT
|
|---|
| 202 | user_id,
|
|---|
| 203 | month_no,
|
|---|
| 204 | month_no - ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY month_no) AS grp
|
|---|
| 205 | FROM monthly_sessions
|
|---|
| 206 | WHERE sessions_count > 0
|
|---|
| 207 | ),
|
|---|
| 208 | longest_streak AS (
|
|---|
| 209 | SELECT
|
|---|
| 210 | user_id,
|
|---|
| 211 | MAX(streak_len) AS longest_active_month_streak
|
|---|
| 212 | FROM (
|
|---|
| 213 | SELECT user_id, grp, COUNT(*) AS streak_len
|
|---|
| 214 | FROM active_month_streaks
|
|---|
| 215 | GROUP BY user_id, grp
|
|---|
| 216 | ) s
|
|---|
| 217 | GROUP BY user_id
|
|---|
| 218 | ),
|
|---|
| 219 | annual_training AS (
|
|---|
| 220 | SELECT
|
|---|
| 221 | user_id,
|
|---|
| 222 | SUM(sessions_count) AS annual_sessions,
|
|---|
| 223 | SUM(total_duration_minutes) AS annual_duration_minutes,
|
|---|
| 224 | SUM(total_calories) AS annual_calories,
|
|---|
| 225 | AVG(total_duration_minutes) AS avg_monthly_duration,
|
|---|
| 226 | AVG(total_calories) AS avg_monthly_calories,
|
|---|
| 227 | COUNT(*) FILTER (WHERE sessions_count > 0) AS active_months,
|
|---|
| 228 | REGR_SLOPE(total_calories::numeric, month_no::numeric) AS calories_trend_slope,
|
|---|
| 229 | REGR_SLOPE(total_duration_minutes::numeric, month_no::numeric) AS duration_trend_slope
|
|---|
| 230 | FROM monthly_sessions
|
|---|
| 231 | GROUP BY user_id
|
|---|
| 232 | ),
|
|---|
| 233 | peak_months AS (
|
|---|
| 234 | SELECT
|
|---|
| 235 | user_id,
|
|---|
| 236 | MAX(month_no) FILTER (WHERE peak_calorie_month_rank = 1) AS peak_calorie_month_no,
|
|---|
| 237 | MAX(month_no) FILTER (WHERE peak_sessions_month_rank = 1) AS peak_sessions_month_no
|
|---|
| 238 | FROM monthly_ranked
|
|---|
| 239 | GROUP BY user_id
|
|---|
| 240 | )
|
|---|
| 241 | SELECT
|
|---|
| 242 | tb.user_id,
|
|---|
| 243 | tb.username,
|
|---|
| 244 | tb.email,
|
|---|
| 245 | tb.gender,
|
|---|
| 246 | tb.age,
|
|---|
| 247 | tb.weight,
|
|---|
| 248 | at.annual_sessions,
|
|---|
| 249 | ROUND(at.annual_duration_minutes::numeric, 2) AS annual_duration_minutes,
|
|---|
| 250 | ROUND(at.annual_calories::numeric, 2) AS annual_calories,
|
|---|
| 251 | at.active_months,
|
|---|
| 252 | ROUND((at.active_months / 12.0)::numeric, 4) AS consistency_ratio,
|
|---|
| 253 | COALESCE(ls.longest_active_month_streak, 0) AS longest_active_month_streak,
|
|---|
| 254 | pm.peak_calorie_month_no,
|
|---|
| 255 | pm.peak_sessions_month_no,
|
|---|
| 256 | ROUND(COALESCE(at.calories_trend_slope, 0)::numeric, 4) AS calories_trend_slope,
|
|---|
| 257 | ROUND(COALESCE(at.duration_trend_slope, 0)::numeric, 4) AS duration_trend_slope,
|
|---|
| 258 | DENSE_RANK() OVER (
|
|---|
| 259 | ORDER BY
|
|---|
| 260 | at.annual_calories DESC,
|
|---|
| 261 | at.active_months DESC,
|
|---|
| 262 | COALESCE(ls.longest_active_month_streak, 0) DESC,
|
|---|
| 263 | tb.user_id ASC
|
|---|
| 264 | ) AS training_annual_rank
|
|---|
| 265 | FROM training_base tb
|
|---|
| 266 | JOIN annual_training at ON at.user_id = tb.user_id
|
|---|
| 267 | JOIN peak_months pm ON pm.user_id = tb.user_id
|
|---|
| 268 | LEFT JOIN longest_streak ls ON ls.user_id = tb.user_id
|
|---|
| 269 | ORDER BY training_annual_rank, tb.user_id;
|
|---|
| 270 | }}}
|
|---|
| 271 |
|
|---|
| 272 | ==== Релациона Алгебра
|
|---|
| 273 | {{{
|
|---|
| 274 | TB <- pi_{tu.user_id, u.username, u.email, tu.gender, tu.age, tu.weight}
|
|---|
| 275 | (training_users tu bowtie_{tu.user_id = u.user_id} users u)
|
|---|
| 276 |
|
|---|
| 277 | TBM <- TB x M
|
|---|
| 278 | TSY <- sigma_{YEAR(ts.date)=Y}(training_sessions ts)
|
|---|
| 279 | MS0 <- TBM leftouterjoin_{TBM.user_id = ts.training_user_id AND TBM.month_no = MONTH(ts.date)} TSY
|
|---|
| 280 | MS <- gamma_{user_id, month_no;
|
|---|
| 281 | COUNT(ts.training_id)->sessions_count,
|
|---|
| 282 | SUM(COALESCE(ts.duration,0))->total_duration_minutes,
|
|---|
| 283 | SUM(COALESCE(ts.calories,0))->total_calories,
|
|---|
| 284 | AVG(COALESCE(ts.duration,0))->avg_session_duration,
|
|---|
| 285 | AVG(COALESCE(ts.calories,0))->avg_session_calories}(MS0)
|
|---|
| 286 |
|
|---|
| 287 | MR <- omega_{PARTITION BY user_id ORDER BY total_calories DESC, month_no ASC;
|
|---|
| 288 | DENSE_RANK()->peak_calorie_month_rank,
|
|---|
| 289 | DENSE_RANK(PARTITION BY user_id ORDER BY sessions_count DESC, month_no ASC)->peak_sessions_month_rank}(MS)
|
|---|
| 290 |
|
|---|
| 291 | AMS <- sigma_{sessions_count>0}(MS)
|
|---|
| 292 | AMS1 <- omega_{PARTITION BY user_id ORDER BY month_no;
|
|---|
| 293 | ROW_NUMBER()->rn}(AMS)
|
|---|
| 294 | AMS2 <- alpha_{month_no - rn -> grp}(AMS1)
|
|---|
| 295 | LS0 <- gamma_{user_id, grp; COUNT(*)->streak_len}(AMS2)
|
|---|
| 296 | LS <- gamma_{user_id; MAX(streak_len)->longest_active_month_streak}(LS0)
|
|---|
| 297 |
|
|---|
| 298 | AT <- gamma_{user_id;
|
|---|
| 299 | SUM(sessions_count)->annual_sessions,
|
|---|
| 300 | SUM(total_duration_minutes)->annual_duration_minutes,
|
|---|
| 301 | SUM(total_calories)->annual_calories,
|
|---|
| 302 | AVG(total_duration_minutes)->avg_monthly_duration,
|
|---|
| 303 | AVG(total_calories)->avg_monthly_calories,
|
|---|
| 304 | COUNT_IF(sessions_count>0)->active_months,
|
|---|
| 305 | REGR_SLOPE(total_calories, month_no)->calories_trend_slope,
|
|---|
| 306 | REGR_SLOPE(total_duration_minutes, month_no)->duration_trend_slope}(MS)
|
|---|
| 307 |
|
|---|
| 308 | PM <- gamma_{user_id;
|
|---|
| 309 | MAX_IF(month_no, peak_calorie_month_rank=1)->peak_calorie_month_no,
|
|---|
| 310 | MAX_IF(month_no, peak_sessions_month_rank=1)->peak_sessions_month_no}(MR)
|
|---|
| 311 |
|
|---|
| 312 | R0 <- TB bowtie_{TB.user_id=AT.user_id} AT
|
|---|
| 313 | bowtie_{TB.user_id=PM.user_id} PM
|
|---|
| 314 | leftouterjoin_{TB.user_id=LS.user_id} LS
|
|---|
| 315 | R1 <- alpha_{active_months/12.0->consistency_ratio,
|
|---|
| 316 | COALESCE(longest_active_month_streak,0)->longest_active_month_streak_nz,
|
|---|
| 317 | COALESCE(calories_trend_slope,0)->calories_trend_slope_nz,
|
|---|
| 318 | COALESCE(duration_trend_slope,0)->duration_trend_slope_nz}(R0)
|
|---|
| 319 | R <- omega_{ORDER BY annual_calories DESC,
|
|---|
| 320 | active_months DESC,
|
|---|
| 321 | longest_active_month_streak_nz DESC,
|
|---|
| 322 | user_id ASC;
|
|---|
| 323 | DENSE_RANK()->training_annual_rank}(R1)
|
|---|
| 324 | }}}
|
|---|
| 325 |
|
|---|
| 326 | === 3. Детален годишен извештај за дисциплина, квалитет на завршување и однесување преку streaks
|
|---|
| 327 |
|
|---|
| 328 | ==== SQL
|
|---|
| 329 | {{{
|
|---|
| 330 | SET search_path TO trekr;
|
|---|
| 331 |
|
|---|
| 332 | WITH params AS (
|
|---|
| 333 | SELECT 2026::int AS report_year
|
|---|
| 334 | ),
|
|---|
| 335 | discipline_base AS (
|
|---|
| 336 | SELECT
|
|---|
| 337 | du.user_id,
|
|---|
| 338 | u.username,
|
|---|
| 339 | u.email
|
|---|
| 340 | FROM discipline_users du
|
|---|
| 341 | JOIN users u ON u.user_id = du.user_id
|
|---|
| 342 | ),
|
|---|
| 343 | task_mix AS (
|
|---|
| 344 | SELECT
|
|---|
| 345 | COALESCE(t.discipline_user_id, c.user_id) AS user_id,
|
|---|
| 346 | COUNT(*) AS total_tasks_defined,
|
|---|
| 347 | COUNT(*) FILTER (WHERE t.custom_tracking_id IS NULL) AS core_tasks,
|
|---|
| 348 | COUNT(*) FILTER (WHERE t.custom_tracking_id IS NOT NULL) AS custom_tasks,
|
|---|
| 349 | COUNT(DISTINCT COALESCE(t.custom_tracking_id::text, 'core')) AS task_category_span
|
|---|
| 350 | FROM tasks t
|
|---|
| 351 | LEFT JOIN custom_tracking_categories c
|
|---|
| 352 | ON c.custom_tracking_id = t.custom_tracking_id
|
|---|
| 353 | WHERE t.discipline_user_id IS NOT NULL
|
|---|
| 354 | OR t.custom_tracking_id IS NOT NULL
|
|---|
| 355 | GROUP BY COALESCE(t.discipline_user_id, c.user_id)
|
|---|
| 356 | ),
|
|---|
| 357 | annual_daily_completion AS (
|
|---|
| 358 | SELECT
|
|---|
| 359 | dc.user_id,
|
|---|
| 360 | dc.date,
|
|---|
| 361 | COALESCE(dc.procent, 0) AS procent,
|
|---|
| 362 | CASE WHEN COALESCE(dc.procent, 0) >= 80 THEN 1 ELSE 0 END AS strong_day
|
|---|
| 363 | FROM daily_completion dc
|
|---|
| 364 | WHERE EXTRACT(YEAR FROM dc.date)::int = (SELECT report_year FROM params)
|
|---|
| 365 | ),
|
|---|
| 366 | daily_completion_stats AS (
|
|---|
| 367 | SELECT
|
|---|
| 368 | adc.user_id,
|
|---|
| 369 | COUNT(*) AS tracked_days,
|
|---|
| 370 | AVG(adc.procent) AS avg_completion_percent,
|
|---|
| 371 | PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY adc.procent) AS median_completion_percent,
|
|---|
| 372 | COUNT(*) FILTER (WHERE adc.procent = 100) AS perfect_days,
|
|---|
| 373 | COUNT(*) FILTER (WHERE adc.procent >= 80) AS strong_days,
|
|---|
| 374 | STDDEV_SAMP(adc.procent) AS completion_variability
|
|---|
| 375 | FROM annual_daily_completion adc
|
|---|
| 376 | GROUP BY adc.user_id
|
|---|
| 377 | ),
|
|---|
| 378 | strong_day_streaks AS (
|
|---|
| 379 | SELECT
|
|---|
| 380 | user_id,
|
|---|
| 381 | date,
|
|---|
| 382 | date - (ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY date))::int AS grp
|
|---|
| 383 | FROM annual_daily_completion
|
|---|
| 384 | WHERE strong_day = 1
|
|---|
| 385 | ),
|
|---|
| 386 | longest_strong_streak AS (
|
|---|
| 387 | SELECT
|
|---|
| 388 | user_id,
|
|---|
| 389 | MAX(streak_len) AS longest_strong_day_streak
|
|---|
| 390 | FROM (
|
|---|
| 391 | SELECT user_id, grp, COUNT(*) AS streak_len
|
|---|
| 392 | FROM strong_day_streaks
|
|---|
| 393 | GROUP BY user_id, grp
|
|---|
| 394 | ) s
|
|---|
| 395 | GROUP BY user_id
|
|---|
| 396 | ),
|
|---|
| 397 | annual_task_execution AS (
|
|---|
| 398 | SELECT
|
|---|
| 399 | dc.user_id,
|
|---|
| 400 | COUNT(tdc.task_id) AS completed_task_events
|
|---|
| 401 | FROM daily_completion dc
|
|---|
| 402 | LEFT JOIN task_daily_completion tdc
|
|---|
| 403 | ON tdc.daily_completion_id = dc.daily_completion_id
|
|---|
| 404 | WHERE EXTRACT(YEAR FROM dc.date)::int = (SELECT report_year FROM params)
|
|---|
| 405 | GROUP BY dc.user_id
|
|---|
| 406 | )
|
|---|
| 407 | SELECT
|
|---|
| 408 | db.user_id,
|
|---|
| 409 | db.username,
|
|---|
| 410 | db.email,
|
|---|
| 411 | COALESCE(tm.total_tasks_defined, 0) AS total_tasks_defined,
|
|---|
| 412 | COALESCE(tm.core_tasks, 0) AS core_tasks,
|
|---|
| 413 | COALESCE(tm.custom_tasks, 0) AS custom_tasks,
|
|---|
| 414 | COALESCE(tm.task_category_span, 0) AS task_category_span,
|
|---|
| 415 | COALESCE(dcs.tracked_days, 0) AS tracked_days,
|
|---|
| 416 | ROUND(COALESCE(dcs.avg_completion_percent, 0)::numeric, 2) AS avg_completion_percent,
|
|---|
| 417 | ROUND(COALESCE(dcs.median_completion_percent, 0)::numeric, 2) AS median_completion_percent,
|
|---|
| 418 | COALESCE(dcs.perfect_days, 0) AS perfect_days,
|
|---|
| 419 | COALESCE(dcs.strong_days, 0) AS strong_days,
|
|---|
| 420 | ROUND(COALESCE(dcs.completion_variability, 0)::numeric, 4) AS completion_variability,
|
|---|
| 421 | COALESCE(ate.completed_task_events, 0) AS completed_task_events,
|
|---|
| 422 | COALESCE(lss.longest_strong_day_streak, 0) AS longest_strong_day_streak,
|
|---|
| 423 | ROUND(
|
|---|
| 424 | COALESCE((COALESCE(dcs.strong_days, 0) / NULLIF(COALESCE(dcs.tracked_days, 0), 0)::numeric), 0),
|
|---|
| 425 | 4
|
|---|
| 426 | ) AS strong_day_ratio,
|
|---|
| 427 | ROUND(
|
|---|
| 428 | (
|
|---|
| 429 | COALESCE(dcs.avg_completion_percent, 0) * 0.45
|
|---|
| 430 | + COALESCE(lss.longest_strong_day_streak, 0) * 2.00
|
|---|
| 431 | + COALESCE(ate.completed_task_events, 0) * 0.35
|
|---|
| 432 | )::numeric,
|
|---|
| 433 | 2
|
|---|
| 434 | ) AS discipline_composite_score,
|
|---|
| 435 | DENSE_RANK() OVER (
|
|---|
| 436 | ORDER BY
|
|---|
| 437 | (
|
|---|
| 438 | COALESCE(dcs.avg_completion_percent, 0) * 0.45
|
|---|
| 439 | + COALESCE(lss.longest_strong_day_streak, 0) * 2.00
|
|---|
| 440 | + COALESCE(ate.completed_task_events, 0) * 0.35
|
|---|
| 441 | ) DESC,
|
|---|
| 442 | db.user_id ASC
|
|---|
| 443 | ) AS discipline_annual_rank
|
|---|
| 444 | FROM discipline_base db
|
|---|
| 445 | LEFT JOIN task_mix tm ON tm.user_id = db.user_id
|
|---|
| 446 | LEFT JOIN daily_completion_stats dcs ON dcs.user_id = db.user_id
|
|---|
| 447 | LEFT JOIN annual_task_execution ate ON ate.user_id = db.user_id
|
|---|
| 448 | LEFT JOIN longest_strong_streak lss ON lss.user_id = db.user_id
|
|---|
| 449 | ORDER BY discipline_annual_rank, db.user_id;
|
|---|
| 450 | }}}
|
|---|
| 451 |
|
|---|
| 452 | ==== Релациона Алгебра
|
|---|
| 453 | {{{
|
|---|
| 454 | DB <- pi_{du.user_id, u.username, u.email}
|
|---|
| 455 | (discipline_users du bowtie_{du.user_id = u.user_id} users u)
|
|---|
| 456 |
|
|---|
| 457 | TC <- tasks t leftouterjoin_{t.custom_tracking_id = c.custom_tracking_id} custom_tracking_categories c
|
|---|
| 458 | TM0 <- alpha_{COALESCE(t.discipline_user_id, c.user_id)->owner_user_id}(TC)
|
|---|
| 459 | TM1 <- sigma_{t.discipline_user_id IS NOT NULL OR t.custom_tracking_id IS NOT NULL}(TM0)
|
|---|
| 460 | TM <- gamma_{owner_user_id;
|
|---|
| 461 | COUNT(*)->total_tasks_defined,
|
|---|
| 462 | COUNT_IF(t.custom_tracking_id IS NULL)->core_tasks,
|
|---|
| 463 | COUNT_IF(t.custom_tracking_id IS NOT NULL)->custom_tasks,
|
|---|
| 464 | COUNT_DISTINCT(COALESCE(t.custom_tracking_id,'core'))->task_category_span}(TM1)
|
|---|
| 465 |
|
|---|
| 466 | ADC0 <- sigma_{YEAR(dc.date)=Y}(daily_completion dc)
|
|---|
| 467 | ADC <- alpha_{COALESCE(dc.procent,0)->procent,
|
|---|
| 468 | CASE(procent>=80,1,0)->strong_day}(ADC0)
|
|---|
| 469 |
|
|---|
| 470 | DCS <- gamma_{user_id;
|
|---|
| 471 | COUNT(*)->tracked_days,
|
|---|
| 472 | AVG(procent)->avg_completion_percent,
|
|---|
| 473 | PERCENTILE_CONT_0_5(procent)->median_completion_percent,
|
|---|
| 474 | COUNT_IF(procent=100)->perfect_days,
|
|---|
| 475 | COUNT_IF(procent>=80)->strong_days,
|
|---|
| 476 | STDDEV_SAMP(procent)->completion_variability}(ADC)
|
|---|
| 477 |
|
|---|
| 478 | SDS0 <- sigma_{strong_day=1}(ADC)
|
|---|
| 479 | SDS1 <- omega_{PARTITION BY user_id ORDER BY date; ROW_NUMBER()->rn}(SDS0)
|
|---|
| 480 | SDS2 <- alpha_{date - rn -> grp}(SDS1)
|
|---|
| 481 | LSS0 <- gamma_{user_id, grp; COUNT(*)->streak_len}(SDS2)
|
|---|
| 482 | LSS <- gamma_{user_id; MAX(streak_len)->longest_strong_day_streak}(LSS0)
|
|---|
| 483 |
|
|---|
| 484 | ATE0 <- ADC0 leftouterjoin_{ADC0.daily_completion_id = tdc.daily_completion_id} task_daily_completion tdc
|
|---|
| 485 | ATE <- gamma_{ADC0.user_id; COUNT(tdc.task_id)->completed_task_events}(ATE0)
|
|---|
| 486 |
|
|---|
| 487 | R0 <- DB
|
|---|
| 488 | leftouterjoin_{DB.user_id = TM.owner_user_id} TM
|
|---|
| 489 | leftouterjoin_{DB.user_id = DCS.user_id} DCS
|
|---|
| 490 | leftouterjoin_{DB.user_id = ATE.user_id} ATE
|
|---|
| 491 | leftouterjoin_{DB.user_id = LSS.user_id} LSS
|
|---|
| 492 | R1 <- alpha_{COALESCE(total_tasks_defined,0)->total_tasks_defined_nz,
|
|---|
| 493 | COALESCE(core_tasks,0)->core_tasks_nz,
|
|---|
| 494 | COALESCE(custom_tasks,0)->custom_tasks_nz,
|
|---|
| 495 | COALESCE(task_category_span,0)->task_category_span_nz,
|
|---|
| 496 | COALESCE(tracked_days,0)->tracked_days_nz,
|
|---|
| 497 | COALESCE(avg_completion_percent,0)->avg_completion_percent_nz,
|
|---|
| 498 | COALESCE(median_completion_percent,0)->median_completion_percent_nz,
|
|---|
| 499 | COALESCE(perfect_days,0)->perfect_days_nz,
|
|---|
| 500 | COALESCE(strong_days,0)->strong_days_nz,
|
|---|
| 501 | COALESCE(completion_variability,0)->completion_variability_nz,
|
|---|
| 502 | COALESCE(completed_task_events,0)->completed_task_events_nz,
|
|---|
| 503 | COALESCE(longest_strong_day_streak,0)->longest_strong_day_streak_nz,
|
|---|
| 504 | COALESCE(strong_days/NULLIF(tracked_days,0),0)->strong_day_ratio,
|
|---|
| 505 | (COALESCE(avg_completion_percent,0)*0.45 +
|
|---|
| 506 | COALESCE(longest_strong_day_streak,0)*2.00 +
|
|---|
| 507 | COALESCE(completed_task_events,0)*0.35)->discipline_composite_score}(R0)
|
|---|
| 508 | R <- omega_{ORDER BY discipline_composite_score DESC, user_id ASC;
|
|---|
| 509 | DENSE_RANK()->discipline_annual_rank}(R1)
|
|---|
| 510 | }}}
|
|---|
| 511 |
|
|---|
| 512 | === 4. Детален годишен извештај за инвестициска диверзификација, концентрација и темпо на вложување
|
|---|
| 513 |
|
|---|
| 514 | ==== SQL
|
|---|
| 515 | {{{
|
|---|
| 516 | SET search_path TO trekr;
|
|---|
| 517 |
|
|---|
| 518 | WITH params AS (
|
|---|
| 519 | SELECT 2026::int AS report_year
|
|---|
| 520 | ),
|
|---|
| 521 | months AS (
|
|---|
| 522 | SELECT generate_series(1, 12) AS month_no
|
|---|
| 523 | ),
|
|---|
| 524 | investor_base AS (
|
|---|
| 525 | SELECT
|
|---|
| 526 | iu.user_id,
|
|---|
| 527 | u.username,
|
|---|
| 528 | u.email
|
|---|
| 529 | FROM investor_users iu
|
|---|
| 530 | JOIN users u ON u.user_id = iu.user_id
|
|---|
| 531 | ),
|
|---|
| 532 | annual_asset_lots AS (
|
|---|
| 533 | SELECT
|
|---|
| 534 | a.user_id,
|
|---|
| 535 | a.ticker_symbol,
|
|---|
| 536 | COALESCE(a.quantity, 0) AS quantity,
|
|---|
| 537 | COALESCE(a.buy_price, 0) AS buy_price,
|
|---|
| 538 | COALESCE(a.quantity, 0) * COALESCE(a.buy_price, 0) AS invested_amount,
|
|---|
| 539 | a.buy_date
|
|---|
| 540 | FROM assets a
|
|---|
| 541 | WHERE EXTRACT(YEAR FROM a.buy_date)::int = (SELECT report_year FROM params)
|
|---|
| 542 | ),
|
|---|
| 543 | ticker_rollup AS (
|
|---|
| 544 | SELECT
|
|---|
| 545 | aal.user_id,
|
|---|
| 546 | aal.ticker_symbol,
|
|---|
| 547 | SUM(aal.quantity) AS total_quantity,
|
|---|
| 548 | SUM(aal.invested_amount) AS total_invested_amount,
|
|---|
| 549 | COUNT(*) AS lot_count,
|
|---|
| 550 | MIN(aal.buy_date) AS first_buy_date,
|
|---|
| 551 | MAX(aal.buy_date) AS last_buy_date
|
|---|
| 552 | FROM annual_asset_lots aal
|
|---|
| 553 | GROUP BY aal.user_id, aal.ticker_symbol
|
|---|
| 554 | ),
|
|---|
| 555 | portfolio_totals AS (
|
|---|
| 556 | SELECT
|
|---|
| 557 | user_id,
|
|---|
| 558 | SUM(total_invested_amount) AS annual_total_invested,
|
|---|
| 559 | SUM(lot_count) AS annual_lot_count,
|
|---|
| 560 | COUNT(*) AS distinct_tickers
|
|---|
| 561 | FROM ticker_rollup
|
|---|
| 562 | GROUP BY user_id
|
|---|
| 563 | ),
|
|---|
| 564 | weights AS (
|
|---|
| 565 | SELECT
|
|---|
| 566 | tr.user_id,
|
|---|
| 567 | tr.ticker_symbol,
|
|---|
| 568 | tr.total_invested_amount,
|
|---|
| 569 | pt.annual_total_invested,
|
|---|
| 570 | (tr.total_invested_amount / NULLIF(pt.annual_total_invested, 0)) AS position_weight,
|
|---|
| 571 | DENSE_RANK() OVER (
|
|---|
| 572 | PARTITION BY tr.user_id
|
|---|
| 573 | ORDER BY tr.total_invested_amount DESC, tr.ticker_symbol ASC
|
|---|
| 574 | ) AS position_rank
|
|---|
| 575 | FROM ticker_rollup tr
|
|---|
| 576 | JOIN portfolio_totals pt ON pt.user_id = tr.user_id
|
|---|
| 577 | ),
|
|---|
| 578 | concentration AS (
|
|---|
| 579 | SELECT
|
|---|
| 580 | user_id,
|
|---|
| 581 | SUM(position_weight * position_weight) AS hhi_concentration,
|
|---|
| 582 | MAX(position_weight) AS top_position_weight,
|
|---|
| 583 | MAX(ticker_symbol) FILTER (WHERE position_rank = 1) AS top_ticker
|
|---|
| 584 | FROM weights
|
|---|
| 585 | GROUP BY user_id
|
|---|
| 586 | ),
|
|---|
| 587 | monthly_investment AS (
|
|---|
| 588 | SELECT
|
|---|
| 589 | ib.user_id,
|
|---|
| 590 | m.month_no,
|
|---|
| 591 | COALESCE(SUM(a.quantity * a.buy_price), 0) AS monthly_invested_amount
|
|---|
| 592 | FROM investor_base ib
|
|---|
| 593 | CROSS JOIN months m
|
|---|
| 594 | LEFT JOIN assets a
|
|---|
| 595 | ON a.user_id = ib.user_id
|
|---|
| 596 | AND EXTRACT(YEAR FROM a.buy_date)::int = (SELECT report_year FROM params)
|
|---|
| 597 | AND EXTRACT(MONTH FROM a.buy_date)::int = m.month_no
|
|---|
| 598 | GROUP BY ib.user_id, m.month_no
|
|---|
| 599 | ),
|
|---|
| 600 | monthly_investment_stats AS (
|
|---|
| 601 | SELECT
|
|---|
| 602 | user_id,
|
|---|
| 603 | AVG(monthly_invested_amount) AS avg_monthly_contribution,
|
|---|
| 604 | STDDEV_SAMP(monthly_invested_amount) AS contribution_stddev,
|
|---|
| 605 | COUNT(*) FILTER (WHERE monthly_invested_amount > 0) AS active_investing_months
|
|---|
| 606 | FROM monthly_investment
|
|---|
| 607 | GROUP BY user_id
|
|---|
| 608 | )
|
|---|
| 609 | SELECT
|
|---|
| 610 | ib.user_id,
|
|---|
| 611 | ib.username,
|
|---|
| 612 | ib.email,
|
|---|
| 613 | COALESCE(pt.annual_total_invested, 0) AS annual_total_invested,
|
|---|
| 614 | COALESCE(pt.annual_lot_count, 0) AS annual_lot_count,
|
|---|
| 615 | COALESCE(pt.distinct_tickers, 0) AS distinct_tickers,
|
|---|
| 616 | ROUND(COALESCE(ms.avg_monthly_contribution, 0)::numeric, 2) AS avg_monthly_contribution,
|
|---|
| 617 | COALESCE(ms.active_investing_months, 0) AS active_investing_months,
|
|---|
| 618 | ROUND((COALESCE(ms.active_investing_months, 0) / 12.0)::numeric, 4) AS activity_ratio,
|
|---|
| 619 | ROUND(COALESCE(c.hhi_concentration, 0)::numeric, 4) AS hhi_concentration,
|
|---|
| 620 | ROUND((1 - COALESCE(c.hhi_concentration, 1))::numeric, 4) AS diversification_index,
|
|---|
| 621 | ROUND(COALESCE(c.top_position_weight, 0)::numeric, 4) AS top_position_weight,
|
|---|
| 622 | c.top_ticker,
|
|---|
| 623 | ROUND((COALESCE(ms.contribution_stddev, 0) / NULLIF(ms.avg_monthly_contribution, 0))::numeric, 4) AS contribution_volatility_cv,
|
|---|
| 624 | DENSE_RANK() OVER (
|
|---|
| 625 | ORDER BY
|
|---|
| 626 | (1 - COALESCE(c.hhi_concentration, 1)) DESC,
|
|---|
| 627 | COALESCE(pt.annual_total_invested, 0) DESC,
|
|---|
| 628 | COALESCE(ms.active_investing_months, 0) DESC,
|
|---|
| 629 | ib.user_id ASC
|
|---|
| 630 | ) AS investing_annual_rank
|
|---|
| 631 | FROM investor_base ib
|
|---|
| 632 | LEFT JOIN portfolio_totals pt ON pt.user_id = ib.user_id
|
|---|
| 633 | LEFT JOIN concentration c ON c.user_id = ib.user_id
|
|---|
| 634 | LEFT JOIN monthly_investment_stats ms ON ms.user_id = ib.user_id
|
|---|
| 635 | ORDER BY investing_annual_rank, ib.user_id;
|
|---|
| 636 | }}}
|
|---|
| 637 |
|
|---|
| 638 | ==== Релациона Алгебра
|
|---|
| 639 | {{{
|
|---|
| 640 | IB <- pi_{iu.user_id, u.username, u.email}
|
|---|
| 641 | (investor_users iu bowtie_{iu.user_id = u.user_id} users u)
|
|---|
| 642 |
|
|---|
| 643 | AAL <- pi_{a.user_id, a.ticker_symbol,
|
|---|
| 644 | COALESCE(a.quantity,0)->quantity,
|
|---|
| 645 | COALESCE(a.buy_price,0)->buy_price,
|
|---|
| 646 | COALESCE(a.quantity,0)*COALESCE(a.buy_price,0)->invested_amount,
|
|---|
| 647 | a.buy_date}
|
|---|
| 648 | (sigma_{YEAR(a.buy_date)=Y}(assets a))
|
|---|
| 649 |
|
|---|
| 650 | TR <- gamma_{user_id, ticker_symbol;
|
|---|
| 651 | SUM(quantity)->total_quantity,
|
|---|
| 652 | SUM(invested_amount)->total_invested_amount,
|
|---|
| 653 | COUNT(*)->lot_count,
|
|---|
| 654 | MIN(buy_date)->first_buy_date,
|
|---|
| 655 | MAX(buy_date)->last_buy_date}(AAL)
|
|---|
| 656 |
|
|---|
| 657 | PT <- gamma_{user_id;
|
|---|
| 658 | SUM(total_invested_amount)->annual_total_invested,
|
|---|
| 659 | SUM(lot_count)->annual_lot_count,
|
|---|
| 660 | COUNT(*)->distinct_tickers}(TR)
|
|---|
| 661 |
|
|---|
| 662 | W0 <- TR bowtie_{TR.user_id = PT.user_id} PT
|
|---|
| 663 | W1 <- alpha_{total_invested_amount/NULLIF(annual_total_invested,0)->position_weight}(W0)
|
|---|
| 664 | W <- omega_{PARTITION BY user_id ORDER BY total_invested_amount DESC, ticker_symbol ASC;
|
|---|
| 665 | DENSE_RANK()->position_rank}(W1)
|
|---|
| 666 |
|
|---|
| 667 | C <- gamma_{user_id;
|
|---|
| 668 | SUM(position_weight*position_weight)->hhi_concentration,
|
|---|
| 669 | MAX(position_weight)->top_position_weight,
|
|---|
| 670 | MAX_IF(ticker_symbol, position_rank=1)->top_ticker}(W)
|
|---|
| 671 |
|
|---|
| 672 | IBM <- IB x M
|
|---|
| 673 | AY <- sigma_{YEAR(a.buy_date)=Y}(assets a)
|
|---|
| 674 | MI0 <- IBM leftouterjoin_{IBM.user_id=a.user_id AND IBM.month_no=MONTH(a.buy_date)} AY
|
|---|
| 675 | MI <- gamma_{user_id, month_no;
|
|---|
| 676 | SUM(COALESCE(a.quantity,0)*COALESCE(a.buy_price,0))->monthly_invested_amount}(MI0)
|
|---|
| 677 | MS <- gamma_{user_id;
|
|---|
| 678 | AVG(monthly_invested_amount)->avg_monthly_contribution,
|
|---|
| 679 | STDDEV_SAMP(monthly_invested_amount)->contribution_stddev,
|
|---|
| 680 | COUNT_IF(monthly_invested_amount>0)->active_investing_months}(MI)
|
|---|
| 681 |
|
|---|
| 682 | R0 <- IB
|
|---|
| 683 | leftouterjoin_{IB.user_id=PT.user_id} PT
|
|---|
| 684 | leftouterjoin_{IB.user_id=C.user_id} C
|
|---|
| 685 | leftouterjoin_{IB.user_id=MS.user_id} MS
|
|---|
| 686 | R1 <- alpha_{COALESCE(annual_total_invested,0)->annual_total_invested_nz,
|
|---|
| 687 | COALESCE(annual_lot_count,0)->annual_lot_count_nz,
|
|---|
| 688 | COALESCE(distinct_tickers,0)->distinct_tickers_nz,
|
|---|
| 689 | COALESCE(avg_monthly_contribution,0)->avg_monthly_contribution_nz,
|
|---|
| 690 | COALESCE(active_investing_months,0)->active_investing_months_nz,
|
|---|
| 691 | COALESCE(active_investing_months,0)/12.0->activity_ratio,
|
|---|
| 692 | COALESCE(hhi_concentration,0)->hhi_concentration_nz,
|
|---|
| 693 | 1-COALESCE(hhi_concentration,1)->diversification_index,
|
|---|
| 694 | COALESCE(top_position_weight,0)->top_position_weight_nz,
|
|---|
| 695 | COALESCE(contribution_stddev/NULLIF(avg_monthly_contribution,0),0)->contribution_volatility_cv}(R0)
|
|---|
| 696 | R <- omega_{ORDER BY diversification_index DESC,
|
|---|
| 697 | annual_total_invested_nz DESC,
|
|---|
| 698 | active_investing_months_nz DESC,
|
|---|
| 699 | user_id ASC;
|
|---|
| 700 | DENSE_RANK()->investing_annual_rank}(R1)
|
|---|
| 701 | }}}
|
|---|
| 702 |
|
|---|