| | 1 | == Advanced Reports |
| | 2 | |
| | 3 | Both reports are implemented as single SQL statements wrapped as callable SQL |
| | 4 | functions in [`schema_creation.sql`](../../server/db/schema_creation.sql): |
| | 5 | |
| | 6 | * `report_top_traders` |
| | 7 | * `report_market_performance` |
| | 8 | |
| | 9 | They are exposed directly in the prototype through menu options `[10]` and `[11]` |
| | 10 | in `server/reports.go`. |
| | 11 | |
| | 12 | == Report 1: Top traders by realized performance |
| | 13 | |
| | 14 | === Description |
| | 15 | |
| | 16 | The purpose of this report is to determine which users achieved the strongest |
| | 17 | trading performance over a selected time period. |
| | 18 | |
| | 19 | For a given interval `[from, to)`, the report calculates: |
| | 20 | |
| | 21 | * **Realized P/L** — the sum of `buy`, `sell` and `fee` transaction amounts. |
| | 22 | Deposits are excluded because they are not trading results. |
| | 23 | * **Total invested** — the absolute value of the user's buy amounts. |
| | 24 | * **ROI %** — realized P/L divided by total invested, multiplied by 100. |
| | 25 | * **Profitable periods** — number of calendar quarters with positive P/L. |
| | 26 | * **Losing periods** — number of calendar quarters with negative P/L. |
| | 27 | * **Total periods** — number of quarters containing any trading activity. |
| | 28 | * **Consistency %** — profitable quarters divided by total active quarters, |
| | 29 | multiplied by 100. |
| | 30 | |
| | 31 | Quarterly grouping is performed internally even when the requested date range |
| | 32 | covers several years. This makes it possible to distinguish a user with one |
| | 33 | large profitable period from a user whose results are consistently positive |
| | 34 | across several periods. |
| | 35 | |
| | 36 | The report is ordered by realized P/L in descending order. |
| | 37 | |
| | 38 | === SQL implementation |
| | 39 | |
| | 40 | Implemented as `project.report_top_traders(p_from, p_to)` in |
| | 41 | [`schema_creation.sql`](../../server/db/schema_creation.sql). |
| | 42 | |
| | 43 | ## [source,sql] |
| | 44 | |
| | 45 | CREATE OR REPLACE FUNCTION project.report_top_traders( |
| | 46 | p_from timestamptz, |
| | 47 | p_to timestamptz |
| | 48 | ) |
| | 49 | RETURNS TABLE ( |
| | 50 | username varchar, |
| | 51 | realized_pl numeric, |
| | 52 | total_invested numeric, |
| | 53 | roi_pct numeric, |
| | 54 | profitable_periods bigint, |
| | 55 | losing_periods bigint, |
| | 56 | total_periods bigint, |
| | 57 | consistency_pct numeric |
| | 58 | ) |
| | 59 | LANGUAGE sql STABLE AS $$ |
| | 60 | WITH period_pl AS ( |
| | 61 | SELECT |
| | 62 | t.user_id, |
| | 63 | date_trunc('quarter', t.created_at) AS period, |
| | 64 | SUM(t.amount) AS period_pl, |
| | 65 | SUM(t.amount) FILTER ( |
| | 66 | WHERE t.type = 'buy' |
| | 67 | ) AS period_buy |
| | 68 | FROM project.transactions t |
| | 69 | WHERE t.type IN ('buy', 'sell', 'fee') |
| | 70 | AND t.created_at >= p_from |
| | 71 | AND t.created_at < p_to |
| | 72 | GROUP BY |
| | 73 | t.user_id, |
| | 74 | date_trunc('quarter', t.created_at) |
| | 75 | ) |
| | 76 | SELECT |
| | 77 | u.username, |
| | 78 | |
| | 79 | ``` |
| | 80 | SUM(pp.period_pl) AS realized_pl, |
| | 81 | |
| | 82 | ABS(SUM(pp.period_buy)) AS total_invested, |
| | 83 | |
| | 84 | ROUND( |
| | 85 | SUM(pp.period_pl) |
| | 86 | / NULLIF(ABS(SUM(pp.period_buy)), 0) * 100, |
| | 87 | 2 |
| | 88 | ) AS roi_pct, |
| | 89 | |
| | 90 | COUNT(*) FILTER ( |
| | 91 | WHERE pp.period_pl > 0 |
| | 92 | ) AS profitable_periods, |
| | 93 | |
| | 94 | COUNT(*) FILTER ( |
| | 95 | WHERE pp.period_pl < 0 |
| | 96 | ) AS losing_periods, |
| | 97 | |
| | 98 | COUNT(*) AS total_periods, |
| | 99 | |
| | 100 | ROUND( |
| | 101 | COUNT(*) FILTER ( |
| | 102 | WHERE pp.period_pl > 0 |
| | 103 | )::numeric |
| | 104 | / NULLIF(COUNT(*), 0) * 100, |
| | 105 | 2 |
| | 106 | ) AS consistency_pct |
| | 107 | |
| | 108 | FROM period_pl pp |
| | 109 | JOIN project.users u |
| | 110 | ON u.id = pp.user_id |
| | 111 | |
| | 112 | GROUP BY |
| | 113 | u.id, |
| | 114 | u.username |
| | 115 | |
| | 116 | ORDER BY |
| | 117 | realized_pl DESC; |
| | 118 | ``` |
| | 119 | |
| | 120 | $$$; |
| | 121 | ---- |
| | 122 | |
| | 123 | === SQL logic |
| | 124 | |
| | 125 | The query consists of one CTE and one outer `SELECT`. |
| | 126 | |
| | 127 | The `period_pl` CTE first filters the transaction history to the requested |
| | 128 | interval and keeps only `buy`, `sell` and `fee` transactions. Transactions are |
| | 129 | then grouped by user and calendar quarter. |
| | 130 | |
| | 131 | For each quarter it calculates: |
| | 132 | |
| | 133 | - the total P/L for that quarter; |
| | 134 | - the total value of buy transactions. |
| | 135 | |
| | 136 | The outer query combines those quarterly results into user-level totals and |
| | 137 | derives ROI and consistency. |
| | 138 | |
| | 139 | `NULLIF` prevents division by zero for users who have no invested amount, while |
| | 140 | the `FILTER` clauses allow profitable and losing quarters to be counted |
| | 141 | independently. |
| | 142 | |
| | 143 | == Report 2: Market performance leaderboard |
| | 144 | |
| | 145 | === Description |
| | 146 | |
| | 147 | The purpose of this report is to compare the performance and activity of all |
| | 148 | markets over a selected time interval. |
| | 149 | |
| | 150 | For each market, the report calculates: |
| | 151 | |
| | 152 | - **Total volume** — sum of traded quantity. |
| | 153 | - **Trade count** — number of market trades. |
| | 154 | - **Average price** — average trade price. |
| | 155 | - **Market return %** — percentage change from the first trade price in the |
| | 156 | period to the last trade price. |
| | 157 | - **Price volatility** — sample standard deviation of trade prices. |
| | 158 | - **Participating users** — number of distinct users with executed orders on the |
| | 159 | market during the period. |
| | 160 | |
| | 161 | `market_trades` is used for volume, trade count, average price and price |
| | 162 | movement because it is the source of executed market activity. |
| | 163 | |
| | 164 | `orders` is used for participating users because `market_trades` intentionally |
| | 165 | does not contain a `user_id`; it records both user-generated fills and simulated |
| | 166 | market activity. |
| | 167 | |
| | 168 | The result is ordered by total traded volume in descending order. |
| | 169 | |
| | 170 | === SQL implementation |
| | 171 | |
| | 172 | Implemented as `project.report_market_performance(p_from, p_to)` in |
| | 173 | [`schema_creation.sql`](../../server/db/schema_creation.sql). |
| | 174 | |
| | 175 | [source,sql] |
| | 176 | ---- |
| | 177 | CREATE OR REPLACE FUNCTION project.report_market_performance( |
| | 178 | p_from timestamptz, |
| | 179 | p_to timestamptz |
| | 180 | ) |
| | 181 | RETURNS TABLE ( |
| | 182 | symbol varchar, |
| | 183 | quote_currency char(3), |
| | 184 | total_volume numeric, |
| | 185 | trade_count bigint, |
| | 186 | avg_price numeric, |
| | 187 | market_return_pct numeric, |
| | 188 | price_volatility numeric, |
| | 189 | participating_users bigint |
| | 190 | ) |
| | 191 | LANGUAGE sql STABLE AS $$ |
| | 192 | WITH trades AS ( |
| | 193 | SELECT |
| | 194 | market_id, |
| | 195 | price, |
| | 196 | quantity, |
| | 197 | executed_at, |
| | 198 | |
| | 199 | FIRST_VALUE(price) OVER w AS first_price, |
| | 200 | |
| | 201 | LAST_VALUE(price) OVER ( |
| | 202 | PARTITION BY market_id |
| | 203 | ORDER BY executed_at |
| | 204 | ROWS BETWEEN UNBOUNDED PRECEDING |
| | 205 | AND UNBOUNDED FOLLOWING |
| | 206 | ) AS last_price |
| | 207 | |
| | 208 | FROM project.market_trades |
| | 209 | |
| | 210 | WHERE executed_at >= p_from |
| | 211 | AND executed_at < p_to |
| | 212 | |
| | 213 | WINDOW w AS ( |
| | 214 | PARTITION BY market_id |
| | 215 | ORDER BY executed_at |
| | 216 | ) |
| | 217 | ), |
| | 218 | |
| | 219 | market_stats AS ( |
| | 220 | SELECT |
| | 221 | market_id, |
| | 222 | SUM(quantity) AS total_volume, |
| | 223 | COUNT(*) AS trade_count, |
| | 224 | AVG(price) AS avg_price, |
| | 225 | STDDEV(price) AS price_volatility, |
| | 226 | MAX(first_price) AS first_price, |
| | 227 | MAX(last_price) AS last_price |
| | 228 | FROM trades |
| | 229 | GROUP BY market_id |
| | 230 | ), |
| | 231 | |
| | 232 | participation AS ( |
| | 233 | SELECT |
| | 234 | market_id, |
| | 235 | COUNT(DISTINCT user_id) AS participating_users |
| | 236 | FROM project.orders |
| | 237 | WHERE status = 'executed' |
| | 238 | AND executed_at >= p_from |
| | 239 | AND executed_at < p_to |
| | 240 | GROUP BY market_id |
| | 241 | ) |
| | 242 | |
| | 243 | SELECT |
| | 244 | c.symbol, |
| | 245 | m.quote_currency, |
| | 246 | |
| | 247 | ms.total_volume, |
| | 248 | ms.trade_count, |
| | 249 | |
| | 250 | ROUND(ms.avg_price, 6) AS avg_price, |
| | 251 | |
| | 252 | ROUND( |
| | 253 | (ms.last_price - ms.first_price) |
| | 254 | / NULLIF(ms.first_price, 0) * 100, |
| | 255 | 2 |
| | 256 | ) AS market_return_pct, |
| | 257 | |
| | 258 | ROUND( |
| | 259 | COALESCE(ms.price_volatility, 0), |
| | 260 | 6 |
| | 261 | ) AS price_volatility, |
| | 262 | |
| | 263 | COALESCE( |
| | 264 | p.participating_users, |
| | 265 | 0 |
| | 266 | ) AS participating_users |
| | 267 | |
| | 268 | FROM market_stats ms |
| | 269 | |
| | 270 | JOIN project.markets m |
| | 271 | ON m.id = ms.market_id |
| | 272 | |
| | 273 | JOIN project.crypto c |
| | 274 | ON c.id = m.crypto_id |
| | 275 | |
| | 276 | LEFT JOIN participation p |
| | 277 | ON p.market_id = ms.market_id |
| | 278 | |
| | 279 | ORDER BY |
| | 280 | ms.total_volume DESC; |
| | 281 | $$; |
| | 282 | ---- |
| | 283 | |
| | 284 | === SQL logic |
| | 285 | |
| | 286 | The `trades` CTE restricts `market_trades` to the requested time interval and |
| | 287 | uses window functions to identify the first and last trade price for each |
| | 288 | market. |
| | 289 | |
| | 290 | The `market_stats` CTE aggregates those trades into one row per market and |
| | 291 | calculates volume, trade count, average price and price volatility. |
| | 292 | |
| | 293 | The `participation` CTE independently counts distinct users with executed |
| | 294 | orders. This must be calculated from `orders` because simulated |
| | 295 | `market_trades` do not belong to individual users. |
| | 296 | |
| | 297 | The final query joins the calculated statistics with `markets` and `crypto` to |
| | 298 | obtain the market symbol and quote currency. |
| | 299 | |
| | 300 | A `LEFT JOIN` is used for participation so that a market with trades but no |
| | 301 | executed user orders still appears in the report with `participating_users = 0`. |
| | 302 | |
| | 303 | == Implementation summary |
| | 304 | |
| | 305 | Both reports are implemented as callable SQL functions and therefore execute |
| | 306 | as part of the actual database-backed prototype rather than existing only as |
| | 307 | documentation. |
| | 308 | |
| | 309 | The two functions are: |
| | 310 | |
| | 311 | - `project.report_top_traders(p_from, p_to)` |
| | 312 | - `project.report_market_performance(p_from, p_to)` |
| | 313 | |
| | 314 | They are exposed through: |
| | 315 | |
| | 316 | - `[10] Report: top traders` |
| | 317 | - `[11] Report: market performance` |
| | 318 | |
| | 319 | No additional database objects or schema changes were necessary because all |
| | 320 | required information already exists in the relational model. |
| | 321 | $$$ |