source: docs/P6-AdvancedReports/AdvancedReports.md@ 35bcb41

main
Last change on this file since 35bcb41 was 35bcb41, checked in by Stefan <trsunovstefan@…>, 13 days ago

Implement Phase 6 complex SQL code

  • Property mode set to 100644
File size: 17.2 KB
Line 
1# Advanced Reports
2
3This is a solo project (see [UseCaseModel](../P3-UseCaseModel/UseCaseModel.md#realization-details-on-selection-of-the-most-important-use-cases)),
4so the rubric's "2 per team member" is 2 reports total. Both are implemented as
5single SQL statements, wrapped as callable SQL functions in
6[`schema_creation.sql`](../../server/db/schema_creation.sql) (`report_top_traders`,
7`report_market_performance`) so they are actual reports inside the prototype — menu
8options `[10]` and `[11]` in `server/reports.go` — not just documentation. No change to
9[ERModel](../P1-ConceptualModel/ERModel.md) or [RelationalDesign](../P2-RelationalDesign/RelationalDesign.md)
10was needed: both reports read `transactions`, `market_trades` and `orders`, all of which
11already carry everything required.
12
13### Notation used below
14
15Both solutions need grouping, aggregation and computed attributes that plain relational
16algebra has no notation for, so the relational-algebra sections use the standard *extended*
17operators:
18
19| Symbol | Meaning |
20|---|---|
21| `σ_cond(R)` | selection |
22| `π_list(R)` | projection — a list entry `expr → name` is a **generalized projection**: a computed attribute, not just a column reference |
23| `ρ_name(R)` | rename |
24| `R ⋈_cond S` | inner join |
25| `R ⟕_cond S` | left outer join (needed wherever a group can legitimately have zero matching rows on the other side, e.g. zero profitable periods, zero participating users) |
26| `γ_{grouping; agg → name, …}(R)` | grouping/aggregation |
27| `τ_attr(R)` | sort, for the presentation order only |
28
29## Top traders by realized performance
30
31### Data requirements description
32
33*"Which users actually made money, how much, how efficiently, and how consistently — over
34a quarter, a year, or several years?"* This is the natural crypto-exchange analogue of "which
35customers bring the most profit" from the phase brief: a Trader's `available_balance` and
36`invested_balance` (P1 `Users`) show a live snapshot, but they say nothing about performance
37*over a chosen window*, and nothing at all about whether a user's results are one lucky
38quarter or a repeatable pattern. All of it is derivable from
39[`transactions`](../../server/db/schema_creation.sql) as it already exists: every buy, sell
40and fee is one signed row there (see [UseCase0004](../P3-UseCaseModel/UseCase0004.md) and
41[UseCase0005](../P3-UseCaseModel/UseCase0005.md) for how each row is produced), so no new
42column or table is needed.
43
44Given a period `[from, to)`:
45
46- **Realized P/L** = `SUM(amount)` over that user's `buy`, `sell` and `fee` transactions in
47 the period (deposits excluded — they are not trading results).
48- **Total invested** = absolute value of the sum of that user's `buy` transactions in the
49 period (buy amounts are stored negative, per
50 [ERModel](../P1-ConceptualModel/ERModel.md#transactions)).
51- **ROI %** = realized P/L ÷ total invested × 100.
52- The period is additionally bucketed into **quarters** internally, regardless of how wide
53 `[from, to)` is, to measure:
54 - **Profitable / losing periods** — how many quarters inside the window had positive vs.
55 negative P/L.
56 - **Consistency %** = profitable periods ÷ total periods with any activity × 100 — two
57 users can have the same total P/L with very different risk profiles, and this is the
58 number that tells them apart.
59
60### Solution SQL
61
62Implemented as `project.report_top_traders(p_from, p_to)` in
63[`schema_creation.sql`](../../server/db/schema_creation.sql):
64
65```sql
66CREATE OR REPLACE FUNCTION project.report_top_traders(p_from timestamptz, p_to timestamptz)
67RETURNS TABLE (
68 username varchar,
69 realized_pl numeric,
70 total_invested numeric,
71 roi_pct numeric,
72 profitable_periods bigint,
73 losing_periods bigint,
74 total_periods bigint,
75 consistency_pct numeric
76)
77LANGUAGE sql STABLE AS $$
78 WITH period_pl AS (
79 SELECT
80 t.user_id,
81 date_trunc('quarter', t.created_at) AS period,
82 SUM(t.amount) AS period_pl,
83 SUM(t.amount) FILTER (WHERE t.type = 'buy') AS period_buy
84 FROM project.transactions t
85 WHERE t.type IN ('buy', 'sell', 'fee')
86 AND t.created_at >= p_from
87 AND t.created_at < p_to
88 GROUP BY t.user_id, date_trunc('quarter', t.created_at)
89 )
90 SELECT
91 u.username,
92 SUM(pp.period_pl) AS realized_pl,
93 ABS(SUM(pp.period_buy)) AS total_invested,
94 ROUND(SUM(pp.period_pl) / NULLIF(ABS(SUM(pp.period_buy)), 0) * 100, 2) AS roi_pct,
95 COUNT(*) FILTER (WHERE pp.period_pl > 0) AS profitable_periods,
96 COUNT(*) FILTER (WHERE pp.period_pl < 0) AS losing_periods,
97 COUNT(*) AS total_periods,
98 ROUND(COUNT(*) FILTER (WHERE pp.period_pl > 0)::numeric
99 / NULLIF(COUNT(*), 0) * 100, 2) AS consistency_pct
100 FROM period_pl pp
101 JOIN project.users u ON u.id = pp.user_id
102 GROUP BY u.id, u.username
103 ORDER BY realized_pl DESC;
104$$;
105```
106
107One `SELECT`, one `WITH` CTE — the CTE does the quarter bucketing per user, the outer query
108rolls those buckets up into the totals, the ROI/consistency percentages and the ranking.
109
110**Verified run.** [`reports_demo_data.sql`](../../server/db/reports_demo_data.sql) adds five
111quarters of round-trip trades (2025-07 through 2026-07) on top of the normal seed data
112specifically so this report has more than one period to work with — see that file's header
113for exactly what it inserts and why it is optional rather than part of `-init`. Run against
114PostgreSQL 16 with `data_load.sql` + `reports_demo_data.sql` loaded, through the actual CLI
115(`[10] Report: top traders`, range `2025-01-01` to `2026-09-17`):
116
117```
118 Username Realized P/L Invested ROI % Prof. Loss Total Consist. %
119 ------------------------------------------------------------------------------------------
120 bob +991.0000 6300.0000 15.73 3 0 3 100.00
121 alice -475.0000 24250.0000 -1.96 2 3 5 40.00
122```
123
124Sorting by raw P/L alone would rank alice above bob if alice's numbers were all positive; here
125it does the opposite, and that is the point of the report — alice traded a much larger total
126(and one of her seeded round trips landed in the same quarter as the ETH buy already in
127`data_load.sql`, tipping that quarter into a loss), while bob's three quarters were smaller
128but every one of them profitable, giving him both the better ROI and a perfect consistency
129score. A single "total profit" column would have hidden that difference completely.
130
131### Solution Relational Algebra
132
133```
134T_period = σ_{type ∈ {buy,sell,fee} ∧ created_at ≥ from ∧ created_at < to} (Transactions)
135
136T_tagged = π_{user_id, created_at, amount,
137 (type = 'buy' ? amount : 0) → buy_amt} (T_period)
138
139Periods = γ_{user_id, quarter(created_at) → period ;
140 SUM(amount) → period_pl, SUM(buy_amt) → period_buy} (T_tagged)
141
142Totals = γ_{user_id ; SUM(period_pl) → realized_pl,
143 ABS(SUM(period_buy)) → total_invested,
144 COUNT(*) → total_periods} (Periods)
145Profitable = γ_{user_id ; COUNT(*) → profitable_periods} (σ_{period_pl > 0} (Periods))
146Losing = γ_{user_id ; COUNT(*) → losing_periods} (σ_{period_pl < 0} (Periods))
147
148Combined = (Totals ⟕_{user_id} Profitable) ⟕_{user_id} Losing
149
150Ranked = π_{user_id, realized_pl, total_invested,
151 (realized_pl / total_invested × 100) → roi_pct,
152 COALESCE(profitable_periods, 0) → profitable_periods,
153 COALESCE(losing_periods, 0) → losing_periods,
154 total_periods,
155 (COALESCE(profitable_periods, 0) / total_periods × 100) → consistency_pct}
156 (Combined)
157
158Result = τ_{realized_pl ↓} (π_{username, realized_pl, total_invested, roi_pct,
159 profitable_periods, losing_periods, total_periods, consistency_pct}
160 (Ranked ⋈_{user_id = id} Users))
161```
162
163`Totals`/`Profitable`/`Losing` are three separate groupings of the same `Periods` relation
164because plain aggregation has no built-in "count only where X" operator; the two outer joins
165recombine them (`⟕`, not `⋈`, because a user with zero losing quarters must still appear with
166`losing_periods = 0`, not disappear from the result).
167
168## Market performance leaderboard
169
170### Data requirements description
171
172*"Which markets were actually worth making — high volume, real price movement, real user
173interest — over a chosen period?"* This is the "products that bring the most profit" /
174"good locations" family of question from the phase brief, translated to markets instead of
175physical products: a market with heavy volume but a dead price, or a big price swing nobody
176actually traded, are both misleading on their own; this report puts volume, trade count,
177price return, volatility and user participation side by side so a market's performance over a
178quarter/year/multi-year window can be judged as a whole, not from one number in isolation.
179Everything needed already exists: `market_trades` is the single source of truth for price and
180volume for every market ([PrototypeImplementation](../P4-Prototype/PrototypeImplementation.md#what-the-prototype-demonstrates-about-the-database-design)),
181and `orders` is the only place a specific user is tied to a specific market
182([ERModel](../P1-ConceptualModel/ERModel.md#placedon--markets-1--orders-n-total-on-orders)) —
183`market_trades` deliberately has no `user_id` column, since it also records the market
184simulator's own fills.
185
186Given a period `[from, to)`, per market:
187
188- **Total volume** = `SUM(quantity)` over its trades in the period.
189- **Trade count** = `COUNT(*)` over the same trades (real fills and simulated fills alike —
190 this is activity, not just user activity).
191- **Average trading price** = `AVG(price)` over the same trades.
192- **Market return %** = `(last trade price − first trade price) ÷ first trade price × 100`,
193 ordering trades by `executed_at` inside the period.
194- **Price volatility** = the (sample) standard deviation of trade prices in the period.
195- **Participating users** = `COUNT(DISTINCT user_id)` from that market's **executed orders**
196 in the period — the only correct source, since `market_trades` cannot answer this question
197 at all.
198
199### Solution SQL
200
201Implemented as `project.report_market_performance(p_from, p_to)` in
202[`schema_creation.sql`](../../server/db/schema_creation.sql):
203
204```sql
205CREATE OR REPLACE FUNCTION project.report_market_performance(p_from timestamptz, p_to timestamptz)
206RETURNS TABLE (
207 symbol varchar,
208 quote_currency char(3),
209 total_volume numeric,
210 trade_count bigint,
211 avg_price numeric,
212 market_return_pct numeric,
213 price_volatility numeric,
214 participating_users bigint
215)
216LANGUAGE sql STABLE AS $$
217 WITH trades AS (
218 SELECT
219 market_id, price, quantity, executed_at,
220 FIRST_VALUE(price) OVER w AS first_price,
221 LAST_VALUE(price) OVER (PARTITION BY market_id ORDER BY executed_at
222 ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS last_price
223 FROM project.market_trades
224 WHERE executed_at >= p_from AND executed_at < p_to
225 WINDOW w AS (PARTITION BY market_id ORDER BY executed_at)
226 ),
227 market_stats AS (
228 SELECT
229 market_id,
230 SUM(quantity) AS total_volume,
231 COUNT(*) AS trade_count,
232 AVG(price) AS avg_price,
233 STDDEV(price) AS price_volatility,
234 MAX(first_price) AS first_price,
235 MAX(last_price) AS last_price
236 FROM trades
237 GROUP BY market_id
238 ),
239 participation AS (
240 SELECT market_id, COUNT(DISTINCT user_id) AS participating_users
241 FROM project.orders
242 WHERE status = 'executed' AND executed_at >= p_from AND executed_at < p_to
243 GROUP BY market_id
244 )
245 SELECT
246 c.symbol,
247 m.quote_currency,
248 ms.total_volume,
249 ms.trade_count,
250 ROUND(ms.avg_price, 6) AS avg_price,
251 ROUND((ms.last_price - ms.first_price) / NULLIF(ms.first_price, 0) * 100, 2) AS market_return_pct,
252 ROUND(COALESCE(ms.price_volatility, 0), 6) AS price_volatility,
253 COALESCE(p.participating_users, 0) AS participating_users
254 FROM market_stats ms
255 JOIN project.markets m ON m.id = ms.market_id
256 JOIN project.crypto c ON c.id = m.crypto_id
257 LEFT JOIN participation p ON p.market_id = ms.market_id
258 ORDER BY ms.total_volume DESC;
259$$;
260```
261
262`FIRST_VALUE`/`LAST_VALUE` pick the period's opening and closing price per market without a
263self-join; `LEFT JOIN participation` is required, not optional — a market can have trades
264from the simulator alone and legitimately zero participating users, and it must still show
265`0`, not disappear from the report.
266
267**Verified run.** Same seed as above (`data_load.sql` + `reports_demo_data.sql`, which also
268adds a BTC/USD uptrend and an ETH/USD downtrend across the same five quarters — see that
269file). Run through the CLI (`[11] Report: market performance`, `2025-01-01` to `2026-09-17`):
270
271```
272 Symbol Quote Volume Trades Avg Price Return % Volatility Users
273 ---------------------------------------------------------------------------------------------
274 DOGE USD 29500.0000 3 0.120583 +2.95 0.001843 0
275 ADA USD 2500.0000 3 0.450750 +1.62 0.003783 0
276 SOL USD 23.5000 3 165.283333 +1.13 0.943840 0
277 ETH USD 14.3500 8 3622.312500 -12.00 182.657992 2
278 BTC USD 3.9750 9 59447.400000 +67.85 10563.413339 2
279```
280
281BTC/USD and ETH/USD are the only two markets with historical (multi-quarter) data seeded, and
282they show it: BTC's price nearly tripled over the period (`+67.85%`) with by far the highest
283volatility, while ETH quietly lost `12%`. ADA/SOL/DOGE only have the few minutes of
284`data_load.sql`'s own recent seed trades, so their return/volatility numbers reflect that
285narrow window, and their `0` participating users is correct — `data_load.sql` seeds trade
286history for every market but only ever places an *order* on ETH.
287
288### Solution Relational Algebra
289
290```
291MT_period = σ_{executed_at ≥ from ∧ executed_at < to} (MarketTrades)
292
293Bounds = γ_{market_id ; MIN(executed_at) → t_first, MAX(executed_at) → t_last} (MT_period)
294
295FirstPx = π_{market_id, price → first_price}
296 (MT_period ⋈_{MT_period.market_id = Bounds.market_id
297 ∧ executed_at = t_first} Bounds)
298LastPx = π_{market_id, price → last_price}
299 (MT_period ⋈_{MT_period.market_id = Bounds.market_id
300 ∧ executed_at = t_last} Bounds)
301
302Stats = γ_{market_id ; SUM(quantity) → total_volume, COUNT(*) → trade_count,
303 AVG(price) → avg_price, STDDEV(price) → price_volatility} (MT_period)
304
305MarketStats = (Stats ⋈_{market_id} FirstPx) ⋈_{market_id} LastPx
306
307O_period = σ_{status = 'executed' ∧ executed_at ≥ from ∧ executed_at < to} (Orders)
308Participation = γ_{market_id ; COUNT_DISTINCT(user_id) → participating_users} (O_period)
309
310Joined = ((MarketStats ⟕_{market_id} Participation)
311 ⋈_{market_id = id} Markets) ⋈_{crypto_id = id} Crypto
312
313Result = τ_{total_volume ↓} (
314 π_{symbol, quote_currency, total_volume, trade_count, avg_price,
315 (last_price − first_price) / first_price × 100 → market_return_pct,
316 COALESCE(price_volatility, 0) → price_volatility,
317 COALESCE(participating_users, 0) → participating_users}
318 (Joined) )
319```
320
321`FirstPx`/`LastPx` express `FIRST_VALUE`/`LAST_VALUE` — which have no classical relational-
322algebra equivalent — as an aggregation for the boundary timestamp per market followed by a
323self-join back to `MarketTrades` to recover the price at that timestamp; this is the standard
324way to express "value at the extreme of a group" in extended relational algebra.
325
326## AI usage
327
328AI was used in this phase and is logged in full, per the course rule for P1 onward.
329
330- **Phase log:** [AdvancedReportsAIUsage.md](AdvancedReportsAIUsage.md) — service used, what
331 the AI produced, and what I decided myself.
332
333**Service:** Claude Code (Anthropic), https://claude.com/claude-code — Claude subscription,
334model Claude Sonnet 5.
335
336**In short:** I specified both report questions in full — including the exact formulas for
337P/L, ROI, consistency, market return, volatility and user participation — and asked the AI to
338turn them into working SQL, wire them into the prototype as real reports, build the
339relational-algebra equivalents, and produce demonstration data rich enough to show the
340reports doing something non-trivial.
Note: See TracBrowser for help on using the repository browser.