Changes between Initial Version and Version 1 of UseCase0006Implementation


Ignore:
Timestamp:
09/24/26 14:03:20 (4 days ago)
Author:
231285
Comment:

--

Legend:

Unmodified
Added
Removed
Modified
  • UseCase0006Implementation

    v1 v1  
     1= Use-case 0006 Implementation - View portfolio and transaction history =
     2
     3'''Initiating actor:''' Trader
     4
     5'''Other actors:''' —
     6
     7A logged-in Trader inspects the current state of their account. The portfolio view
     8lists every cryptocurrency the Trader holds with the quantity (also split into the
     9part reserved by open sell orders and the part that is free to sell), the average
     10buy price, the current market price, the market value and the unrealised
     11profit/loss, followed by a totals row and a cash summary (cash available, portfolio
     12value, net worth). The transaction history lists the Trader's last 20 ledger
     13entries — deposits, buys and sells — newest first. Both are read-only: nothing in
     14the database is changed.
     15
     16Original use-case description (P3): [wiki:UseCase0006].
     17Implementation: `server/portfolio.go`, function `ShowPortfolio`, and
     18`server/account.go`, function `ShowTransactions` (the code is shown at the end of
     19this page).
     20
     21Precondition: the Trader is logged in ([wiki:UseCase0002Implementation UseCase0002]).
     22The 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,
     258250.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{{{
     36SELECT 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{{{
     53SELECT available_balance, invested_balance FROM users WHERE id = $1
     54}}}
     55
     56and prints `Cash available` (= `available_balance`), `Portfolio value`
     57(= the total market value) and `Net worth` (= their sum).
     58
     59The screenshot shows the result of steps 2–3. The table is wider than the
     60terminal window, so each long line wraps and the header row has scrolled out of
     61the 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
     78Checking the figures: ETH is 0.5 − 0.2 = 0.3 at an average buy price of 3500 and
     79a current price of 3520, so value 1056.00 and P/L 0.3 × 20 = +6.00; BTC was just
     80bought at the current price 67140, so its P/L is 0. Cash is
     818250.00 − 671.40 (buy) + 704.00 (sell) = 8282.60. `Reserved` is 0.0000 for both
     82because the market orders executed immediately — a quantity is reserved only
     83while 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{{{
     94SELECT 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
     101The screenshot shows steps 1–2: the choice `7` and the four ledger rows of this
     102run — the sell of 0.2 ETH (+704.0000 USD), the buy of 0.01 BTC (−671.4000 USD),
     103and the two seed rows, the initial deposit of 10000.0000 USD and the seed buy of
     1040.5 ETH (−1750.0000 USD). Buys are stored with a negative amount, deposits and
     105sells with a positive one. The two seed rows were inserted by `data_load.sql` in
     106one statement and have the same `created_at`, so their relative order is not
     107determined by the `ORDER BY`.
     108
     109[[Image(uc0006_history.png)]]
     110
     111All statements run on the `project` schema (the connection sets
     112`search_path=project,public`).
     113
     114=== Reference — how v_portfolio is defined ===
     115
     116From `server/db/schema_creation.sql`:
     117
     118{{{
     119CREATE OR REPLACE VIEW project.v_portfolio AS
     120SELECT 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
     129FROM   project.holdings h
     130JOIN   project.crypto   c ON c.id = h.crypto_id
     131LEFT   JOIN project.markets m ON m.crypto_id = c.id AND m.quote_currency = 'USD'
     132LEFT   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
     146Both screenshots come from one real run (portfolio and history taken right after
     147the 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.
     156func 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.
     222func 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}}}