| 1 | = Use-case 0006 Implementation - View portfolio and transaction history =
|
|---|
| 2 |
|
|---|
| 3 | '''Initiating actor:''' Trader
|
|---|
| 4 |
|
|---|
| 5 | '''Other actors:''' —
|
|---|
| 6 |
|
|---|
| 7 | A logged-in Trader inspects the current state of their account. The portfolio view
|
|---|
| 8 | lists every cryptocurrency the Trader holds with the quantity (also split into the
|
|---|
| 9 | part reserved by open sell orders and the part that is free to sell), the average
|
|---|
| 10 | buy price, the current market price, the market value and the unrealised
|
|---|
| 11 | profit/loss, followed by a totals row and a cash summary (cash available, portfolio
|
|---|
| 12 | value, net worth). The transaction history lists the Trader's last 20 ledger
|
|---|
| 13 | entries — deposits, buys and sells — newest first. Both are read-only: nothing in
|
|---|
| 14 | the database is changed.
|
|---|
| 15 |
|
|---|
| 16 | Original use-case description (P3): [wiki:UseCase0006].
|
|---|
| 17 | Implementation: `server/portfolio.go`, function `ShowPortfolio`, and
|
|---|
| 18 | `server/account.go`, function `ShowTransactions` (the code is shown at the end of
|
|---|
| 19 | this page).
|
|---|
| 20 |
|
|---|
| 21 | Precondition: the Trader is logged in ([wiki:UseCase0002Implementation UseCase0002]).
|
|---|
| 22 | The run below is alice's, after she bought 0.01 BTC
|
|---|
| 23 | ([wiki:UseCase0004Implementation UseCase0004]) and sold 0.2 ETH
|
|---|
| 24 | ([wiki:UseCase0005Implementation UseCase0005]) on top of the seed data (0.5 ETH,
|
|---|
| 25 | 8250.00 USD cash).
|
|---|
| 26 |
|
|---|
| 27 | == Scenario ==
|
|---|
| 28 |
|
|---|
| 29 | === Portfolio ===
|
|---|
| 30 |
|
|---|
| 31 | 1. '''Trader''' chooses `[6] View portfolio` in the authenticated menu (types `6`).
|
|---|
| 32 | The menu is the one shown in [wiki:UseCase0002Implementation UseCase0002], step 7.
|
|---|
| 33 | 2. '''System''' queries the `v_portfolio` view (`$1` = user id):
|
|---|
| 34 |
|
|---|
| 35 | {{{
|
|---|
| 36 | SELECT symbol,
|
|---|
| 37 | quantity,
|
|---|
| 38 | COALESCE(reserved_quantity, 0),
|
|---|
| 39 | COALESCE(available_quantity, quantity),
|
|---|
| 40 | COALESCE(avg_price, 0),
|
|---|
| 41 | COALESCE(current_price, 0),
|
|---|
| 42 | COALESCE(market_value, 0),
|
|---|
| 43 | COALESCE(unrealized_pnl, 0)
|
|---|
| 44 | FROM v_portfolio
|
|---|
| 45 | WHERE user_id = $1 AND quantity > 0
|
|---|
| 46 | ORDER BY symbol
|
|---|
| 47 | }}}
|
|---|
| 48 |
|
|---|
| 49 | 3. '''System''' displays the rows and a `TOTAL` row (sums of the value and P/L columns,
|
|---|
| 50 | computed in Go), then reads the cash balance for the summary (`$1` = user id):
|
|---|
| 51 |
|
|---|
| 52 | {{{
|
|---|
| 53 | SELECT available_balance, invested_balance FROM users WHERE id = $1
|
|---|
| 54 | }}}
|
|---|
| 55 |
|
|---|
| 56 | and prints `Cash available` (= `available_balance`), `Portfolio value`
|
|---|
| 57 | (= the total market value) and `Net worth` (= their sum).
|
|---|
| 58 |
|
|---|
| 59 | The screenshot shows the result of steps 2–3. The table is wider than the
|
|---|
| 60 | terminal window, so each long line wraps and the header row has scrolled out of
|
|---|
| 61 | the top of the window; the complete output of this run is reproduced below it.
|
|---|
| 62 |
|
|---|
| 63 | [[Image(uc0006_portfolio.png)]]
|
|---|
| 64 |
|
|---|
| 65 | {{{
|
|---|
| 66 | Symbol Quantity Reserved Available Avg buy Current Value Unrealised P/L
|
|---|
| 67 | ------------------------------------------------------------------------------------------------------------------
|
|---|
| 68 | BTC 0.0100 0.0000 0.0100 67140.000000 67140.000000 671.4000 +0.0000
|
|---|
| 69 | ETH 0.3000 0.0000 0.3000 3500.000000 3520.000000 1056.0000 +6.0000
|
|---|
| 70 | ------------------------------------------------------------------------------------------------------------------
|
|---|
| 71 | TOTAL 1727.4000 +6.0000
|
|---|
| 72 |
|
|---|
| 73 | Cash available : 8282.6000 USD
|
|---|
| 74 | Portfolio value: 1727.4000 USD
|
|---|
| 75 | Net worth : 10010.0000 USD
|
|---|
| 76 | }}}
|
|---|
| 77 |
|
|---|
| 78 | Checking the figures: ETH is 0.5 − 0.2 = 0.3 at an average buy price of 3500 and
|
|---|
| 79 | a current price of 3520, so value 1056.00 and P/L 0.3 × 20 = +6.00; BTC was just
|
|---|
| 80 | bought at the current price 67140, so its P/L is 0. Cash is
|
|---|
| 81 | 8250.00 − 671.40 (buy) + 704.00 (sell) = 8282.60. `Reserved` is 0.0000 for both
|
|---|
| 82 | because the market orders executed immediately — a quantity is reserved only
|
|---|
| 83 | while a sell order is still open.
|
|---|
| 84 |
|
|---|
| 85 | === Transaction history ===
|
|---|
| 86 |
|
|---|
| 87 | 1. '''Trader''' chooses `[7] View transaction history` in the authenticated menu
|
|---|
| 88 | (types `7`).
|
|---|
| 89 | 2. '''System''' queries the last 20 ledger entries of the Trader (`$1` = user id) and
|
|---|
| 90 | prints them, newest first (the time is shown as the first 19 characters of
|
|---|
| 91 | `created_at`):
|
|---|
| 92 |
|
|---|
| 93 | {{{
|
|---|
| 94 | SELECT created_at, type, amount, currency, COALESCE(description, '')
|
|---|
| 95 | FROM transactions
|
|---|
| 96 | WHERE user_id = $1
|
|---|
| 97 | ORDER BY created_at DESC
|
|---|
| 98 | LIMIT 20
|
|---|
| 99 | }}}
|
|---|
| 100 |
|
|---|
| 101 | The screenshot shows steps 1–2: the choice `7` and the four ledger rows of this
|
|---|
| 102 | run — the sell of 0.2 ETH (+704.0000 USD), the buy of 0.01 BTC (−671.4000 USD),
|
|---|
| 103 | and the two seed rows, the initial deposit of 10000.0000 USD and the seed buy of
|
|---|
| 104 | 0.5 ETH (−1750.0000 USD). Buys are stored with a negative amount, deposits and
|
|---|
| 105 | sells with a positive one. The two seed rows were inserted by `data_load.sql` in
|
|---|
| 106 | one statement and have the same `created_at`, so their relative order is not
|
|---|
| 107 | determined by the `ORDER BY`.
|
|---|
| 108 |
|
|---|
| 109 | [[Image(uc0006_history.png)]]
|
|---|
| 110 |
|
|---|
| 111 | All statements run on the `project` schema (the connection sets
|
|---|
| 112 | `search_path=project,public`).
|
|---|
| 113 |
|
|---|
| 114 | === Reference — how v_portfolio is defined ===
|
|---|
| 115 |
|
|---|
| 116 | From `server/db/schema_creation.sql`:
|
|---|
| 117 |
|
|---|
| 118 | {{{
|
|---|
| 119 | CREATE OR REPLACE VIEW project.v_portfolio AS
|
|---|
| 120 | SELECT h.user_id,
|
|---|
| 121 | c.symbol,
|
|---|
| 122 | h.quantity,
|
|---|
| 123 | h.reserved_quantity,
|
|---|
| 124 | (h.quantity - h.reserved_quantity) AS available_quantity,
|
|---|
| 125 | h.avg_price,
|
|---|
| 126 | lp.price AS current_price,
|
|---|
| 127 | (h.quantity * lp.price) AS market_value,
|
|---|
| 128 | (h.quantity * (lp.price - h.avg_price)) AS unrealized_pnl
|
|---|
| 129 | FROM project.holdings h
|
|---|
| 130 | JOIN project.crypto c ON c.id = h.crypto_id
|
|---|
| 131 | LEFT JOIN project.markets m ON m.crypto_id = c.id AND m.quote_currency = 'USD'
|
|---|
| 132 | LEFT JOIN project.v_latest_prices lp ON lp.market_id = m.id;
|
|---|
| 133 | }}}
|
|---|
| 134 |
|
|---|
| 135 | == How to reproduce ==
|
|---|
| 136 |
|
|---|
| 137 | {{{
|
|---|
| 138 | ./eduberza -init
|
|---|
| 139 | ./eduberza
|
|---|
| 140 | # [2] Login: alice / test123
|
|---|
| 141 | # [4] buy 0.01 BTC, [5] sell 0.2 ETH (UseCase0004 / UseCase0005)
|
|---|
| 142 | # [6] View portfolio
|
|---|
| 143 | # [7] View transaction history
|
|---|
| 144 | }}}
|
|---|
| 145 |
|
|---|
| 146 | Both screenshots come from one real run (portfolio and history taken right after
|
|---|
| 147 | the buy and sell runs).
|
|---|
| 148 |
|
|---|
| 149 | == Source code ==
|
|---|
| 150 |
|
|---|
| 151 | `server/portfolio.go` — `ShowPortfolio`:
|
|---|
| 152 |
|
|---|
| 153 | {{{
|
|---|
| 154 | // ShowPortfolio - UC0006
|
|---|
| 155 | // Uses the v_portfolio view to list holdings with current market value and P&L.
|
|---|
| 156 | func ShowPortfolio(s *Session) {
|
|---|
| 157 | rows, err := db.DB.Query(
|
|---|
| 158 | `SELECT symbol,
|
|---|
| 159 | quantity,
|
|---|
| 160 | COALESCE(reserved_quantity, 0),
|
|---|
| 161 | COALESCE(available_quantity, quantity),
|
|---|
| 162 | COALESCE(avg_price, 0),
|
|---|
| 163 | COALESCE(current_price, 0),
|
|---|
| 164 | COALESCE(market_value, 0),
|
|---|
| 165 | COALESCE(unrealized_pnl, 0)
|
|---|
| 166 | FROM v_portfolio
|
|---|
| 167 | WHERE user_id = $1 AND quantity > 0
|
|---|
| 168 | ORDER BY symbol`,
|
|---|
| 169 | s.UserID,
|
|---|
| 170 | )
|
|---|
| 171 | if err != nil {
|
|---|
| 172 | fmt.Println("Error:", err)
|
|---|
| 173 | return
|
|---|
| 174 | }
|
|---|
| 175 | defer rows.Close()
|
|---|
| 176 |
|
|---|
| 177 | header := fmt.Sprintf(" %-8s %12s %12s %12s %14s %14s %14s %14s",
|
|---|
| 178 | "Symbol", "Quantity", "Reserved", "Available", "Avg buy", "Current", "Value", "Unrealised P/L")
|
|---|
| 179 | fmt.Println()
|
|---|
| 180 | fmt.Println(header)
|
|---|
| 181 | fmt.Println(" " + strings.Repeat("-", len(header)-2))
|
|---|
| 182 |
|
|---|
| 183 | var totalValue, totalPnL float64
|
|---|
| 184 | empty := true
|
|---|
| 185 | for rows.Next() {
|
|---|
| 186 | var sym string
|
|---|
| 187 | var qty, reserved, avail, avg, cur, val, pnl float64
|
|---|
| 188 | if err := rows.Scan(&sym, &qty, &reserved, &avail, &avg, &cur, &val, &pnl); err != nil {
|
|---|
| 189 | fmt.Println("scan error:", err)
|
|---|
| 190 | return
|
|---|
| 191 | }
|
|---|
| 192 | fmt.Printf(" %-8s %12.4f %12.4f %12.4f %14.6f %14.6f %14.4f %+14.4f\n",
|
|---|
| 193 | sym, qty, reserved, avail, avg, cur, val, pnl)
|
|---|
| 194 | totalValue += val
|
|---|
| 195 | totalPnL += pnl
|
|---|
| 196 | empty = false
|
|---|
| 197 | }
|
|---|
| 198 | if empty {
|
|---|
| 199 | fmt.Println(" (no holdings yet)")
|
|---|
| 200 | return
|
|---|
| 201 | }
|
|---|
| 202 | fmt.Println(" " + strings.Repeat("-", len(header)-2))
|
|---|
| 203 | fmt.Printf(" %-8s %12s %12s %12s %14s %14s %14.4f %+14.4f\n",
|
|---|
| 204 | "TOTAL", "", "", "", "", "", totalValue, totalPnL)
|
|---|
| 205 |
|
|---|
| 206 | // cash summary
|
|---|
| 207 | var avail, invested float64
|
|---|
| 208 | _ = db.DB.QueryRow(
|
|---|
| 209 | `SELECT available_balance, invested_balance FROM users WHERE id = $1`,
|
|---|
| 210 | s.UserID,
|
|---|
| 211 | ).Scan(&avail, &invested)
|
|---|
| 212 | fmt.Printf("\n Cash available : %.4f USD\n", avail)
|
|---|
| 213 | fmt.Printf(" Portfolio value: %.4f USD\n", totalValue)
|
|---|
| 214 | fmt.Printf(" Net worth : %.4f USD\n", avail+totalValue)
|
|---|
| 215 | }
|
|---|
| 216 | }}}
|
|---|
| 217 |
|
|---|
| 218 | `server/account.go` — `ShowTransactions`:
|
|---|
| 219 |
|
|---|
| 220 | {{{
|
|---|
| 221 | // ShowTransactions lists the last 20 ledger entries for the user.
|
|---|
| 222 | func ShowTransactions(s *Session) {
|
|---|
| 223 | rows, err := db.DB.Query(
|
|---|
| 224 | `SELECT created_at, type, amount, currency, COALESCE(description, '')
|
|---|
| 225 | FROM transactions
|
|---|
| 226 | WHERE user_id = $1
|
|---|
| 227 | ORDER BY created_at DESC
|
|---|
| 228 | LIMIT 20`,
|
|---|
| 229 | s.UserID,
|
|---|
| 230 | )
|
|---|
| 231 | if err != nil {
|
|---|
| 232 | fmt.Println("Error:", err)
|
|---|
| 233 | return
|
|---|
| 234 | }
|
|---|
| 235 | defer rows.Close()
|
|---|
| 236 |
|
|---|
| 237 | fmt.Println()
|
|---|
| 238 | fmt.Printf(" %-20s %-8s %12s %-3s %s\n", "When", "Type", "Amount", "Cur", "Description")
|
|---|
| 239 | fmt.Println(" " + strings.Repeat("-", 70))
|
|---|
| 240 | for rows.Next() {
|
|---|
| 241 | var when, typ, cur, desc string
|
|---|
| 242 | var amt float64
|
|---|
| 243 | if err := rows.Scan(&when, &typ, &amt, &cur, &desc); err != nil {
|
|---|
| 244 | fmt.Println("scan error:", err)
|
|---|
| 245 | return
|
|---|
| 246 | }
|
|---|
| 247 | fmt.Printf(" %-20s %-8s %12.4f %-3s %s\n", when[:19], typ, amt, cur, desc)
|
|---|
| 248 | }
|
|---|
| 249 | }
|
|---|
| 250 | }}}
|
|---|