| 1 | # Use-case 0004 Implementation - Place market BUY order
|
|---|
| 2 |
|
|---|
| 3 | **Initiating actor:** Trader
|
|---|
| 4 |
|
|---|
| 5 | **Other actors:** Market Simulator (indirect — supplies the current price via `market_trades`).
|
|---|
| 6 |
|
|---|
| 7 | A logged-in Trader buys a crypto asset at the current market price. The Trader never
|
|---|
| 8 | types a symbol or an identifier: the system lists the active markets with their last
|
|---|
| 9 | price, numbered, and the Trader picks one by its number and then enters only the
|
|---|
| 10 | quantity. The system checks that the Trader has enough available cash for
|
|---|
| 11 | quantity × price and then, in one database transaction, records the order, moves the
|
|---|
| 12 | cash from available to invested, adds the crypto to the Trader's holding (recomputing
|
|---|
| 13 | the weighted-average entry price), writes a ledger entry and a market trade, and marks
|
|---|
| 14 | the order executed. The operation touches five tables (`orders`, `users`, `holdings`,
|
|---|
| 15 | `transactions`, `market_trades`) and either all of it succeeds or all of it is rolled
|
|---|
| 16 | back.
|
|---|
| 17 |
|
|---|
| 18 | Original use-case description (P3): [UseCase0004](../P3-UseCaseModel/UseCase0004.md).
|
|---|
| 19 | Implementation: [`server/trade.go`](../../server/trade.go), function
|
|---|
| 20 | `PlaceOrder(s, "buy")` (with `upsertHoldingOnBuy` in the same file), which calls
|
|---|
| 21 | `ChooseMarket`, `ListMarkets`, `pickNumber` and `LatestPrice` from
|
|---|
| 22 | [`server/market.go`](../../server/market.go).
|
|---|
| 23 |
|
|---|
| 24 | All statements run on the `project` schema: the connection sets
|
|---|
| 25 | `search_path=project,public` (`server/db/db.go`), so `orders` means `project.orders`.
|
|---|
| 26 | The SQL below is copied from the Go code; only the Go source indentation is removed,
|
|---|
| 27 | a `;` is added after each statement of the transaction, and `--` comments say what
|
|---|
| 28 | each `$n` placeholder is bound to.
|
|---|
| 29 |
|
|---|
| 30 | The run shown is user `alice` on the seed data (available 8250.00 USD, holding
|
|---|
| 31 | 0.5 ETH bought at 3500), buying 0.01 BTC.
|
|---|
| 32 |
|
|---|
| 33 | ## Scenario
|
|---|
| 34 |
|
|---|
| 35 | 1. **Trader** chooses `[4] Place market BUY order` in the authenticated menu (types `4`).
|
|---|
| 36 | 2. **System** prints `-- Place market buy order --` and lists all active markets,
|
|---|
| 37 | numbered, with their last price (`ListMarkets`, called by `ChooseMarket`):
|
|---|
| 38 |
|
|---|
| 39 | ```sql
|
|---|
| 40 | SELECT m.id, c.id, c.symbol, m.quote_currency,
|
|---|
| 41 | COALESCE(lp.price, 0) AS price
|
|---|
| 42 | FROM markets m
|
|---|
| 43 | JOIN crypto c ON c.id = m.crypto_id
|
|---|
| 44 | LEFT JOIN v_latest_prices lp ON lp.market_id = m.id
|
|---|
| 45 | WHERE m.is_active = true
|
|---|
| 46 | ORDER BY c.symbol
|
|---|
| 47 | ```
|
|---|
| 48 |
|
|---|
| 49 | The rows are printed in this order as `1 ADA`, `2 BTC`, `3 DOGE`, `4 ETH`, `5 SOL`;
|
|---|
| 50 | Go keeps each row's market id and crypto id in memory, so the Trader only ever sees
|
|---|
| 51 | and types the list number. The system then asks `Market #:`.
|
|---|
| 52 |
|
|---|
| 53 | 
|
|---|
| 54 |
|
|---|
| 55 | 3. **Trader** picks the market by its number in the list: `2` (BTC/USD).
|
|---|
| 56 | 4. **System** takes the market id and crypto id of row 2 from the list (no further
|
|---|
| 57 | lookup by symbol) and reads the latest price of that market (`LatestPrice`;
|
|---|
| 58 | `$1` = the chosen market's id):
|
|---|
| 59 |
|
|---|
| 60 | ```sql
|
|---|
| 61 | SELECT price FROM v_latest_prices WHERE market_id = $1
|
|---|
| 62 | ```
|
|---|
| 63 |
|
|---|
| 64 | It prints `Latest price for BTC/USD = 67140.000000` and asks `Quantity:`.
|
|---|
| 65 |
|
|---|
| 66 | 
|
|---|
| 67 |
|
|---|
| 68 | 5. **Trader** enters the quantity `0.01`.
|
|---|
| 69 | 6. **System** computes in Go notional = quantity × price = 0.01 × 67140 = 671.40 and
|
|---|
| 70 | passes it to SQL as a parameter. It then runs one database transaction; the
|
|---|
| 71 | statements below are in exactly the order `PlaceOrder` executes them for a buy:
|
|---|
| 72 |
|
|---|
| 73 | ```sql
|
|---|
| 74 | BEGIN;
|
|---|
| 75 |
|
|---|
| 76 | -- (a) record the order as 'open' — no trade has happened yet.
|
|---|
| 77 | -- $1 = user id, $2 = market id, $3 = side (the Go variable side = 'buy'),
|
|---|
| 78 | -- $4 = quantity (0.01), $5 = price (67140); the returned id is kept in Go.
|
|---|
| 79 | INSERT INTO orders (user_id, market_id, side, type, status, quantity, price)
|
|---|
| 80 | VALUES ($1, $2, $3, 'market', 'open', $4, $5)
|
|---|
| 81 | RETURNING id;
|
|---|
| 82 |
|
|---|
| 83 | -- (b) lock the user's row and read the available cash. $1 = user id.
|
|---|
| 84 | -- Go compares it with the notional; if it is smaller -> alternate flow 6a.
|
|---|
| 85 | SELECT available_balance FROM users WHERE id = $1 FOR UPDATE;
|
|---|
| 86 |
|
|---|
| 87 | -- (c) move the notional from available to invested cash.
|
|---|
| 88 | -- $1 = notional (671.40), $2 = user id.
|
|---|
| 89 | UPDATE users
|
|---|
| 90 | SET available_balance = available_balance - $1,
|
|---|
| 91 | invested_balance = invested_balance + $1,
|
|---|
| 92 | updated_at = now()
|
|---|
| 93 | WHERE id = $2;
|
|---|
| 94 |
|
|---|
| 95 | -- (d) add the crypto to the holding (upsertHoldingOnBuy), recomputing the
|
|---|
| 96 | -- weighted-average entry price in the database. Every SET expression sees
|
|---|
| 97 | -- the pre-update row, so holdings.quantity is still the old quantity.
|
|---|
| 98 | -- $1 = user id, $2 = crypto id, $3 = quantity (0.01), $4 = price (67140).
|
|---|
| 99 | INSERT INTO holdings (user_id, crypto_id, quantity, avg_price, updated_at)
|
|---|
| 100 | VALUES ($1, $2, $3, $4, now())
|
|---|
| 101 | ON CONFLICT (user_id, crypto_id) DO UPDATE
|
|---|
| 102 | SET avg_price = (holdings.quantity * holdings.avg_price
|
|---|
| 103 | + EXCLUDED.quantity * EXCLUDED.avg_price)
|
|---|
| 104 | / (holdings.quantity + EXCLUDED.quantity),
|
|---|
| 105 | quantity = holdings.quantity + EXCLUDED.quantity,
|
|---|
| 106 | updated_at = now();
|
|---|
| 107 |
|
|---|
| 108 | -- (e) ledger entry. $1 = user id, $2 = -notional (-671.40), $3 = order id from (a),
|
|---|
| 109 | -- $4 = description built in Go: 'Market buy 0.0100 BTC @ 67140.000000'.
|
|---|
| 110 | INSERT INTO transactions (user_id, type, amount, currency, related_order, description)
|
|---|
| 111 | VALUES ($1, 'buy', $2, 'USD', $3, $4);
|
|---|
| 112 |
|
|---|
| 113 | -- (f) record the resulting market trade.
|
|---|
| 114 | -- $1 = market id, $2 = price, $3 = quantity, $4 = side ('buy').
|
|---|
| 115 | INSERT INTO market_trades (market_id, executed_at, price, quantity, side, source)
|
|---|
| 116 | VALUES ($1, now(), $2, $3, $4, 'user');
|
|---|
| 117 |
|
|---|
| 118 | -- (g) settle the order itself — it has now actually been filled. $1 = order id.
|
|---|
| 119 | UPDATE orders SET status = 'executed', executed_at = now() WHERE id = $1;
|
|---|
| 120 |
|
|---|
| 121 | COMMIT;
|
|---|
| 122 | ```
|
|---|
| 123 |
|
|---|
| 124 | A buy never reserves crypto (only a sell does, see
|
|---|
| 125 | [UseCase0005](UseCase0005Implementation.md)), so `holdings.reserved_quantity` is not
|
|---|
| 126 | touched and stays 0.
|
|---|
| 127 |
|
|---|
| 128 | 7. **System** confirms
|
|---|
| 129 | `Order executed: buy 0.0100 BTC @ 67140.000000 (notional 671.4000 USD)` and shows
|
|---|
| 130 | the authenticated menu again.
|
|---|
| 131 |
|
|---|
| 132 | The screenshot shows steps 5–7: the entered quantity, the confirmation and the menu.
|
|---|
| 133 |
|
|---|
| 134 | 
|
|---|
| 135 |
|
|---|
| 136 | ### Verification — portfolio after the buy
|
|---|
| 137 |
|
|---|
| 138 | Right after the buy the Trader chooses `[6] View portfolio`
|
|---|
| 139 | ([UseCase0006](UseCase0006Implementation.md)). It shows the new holding
|
|---|
| 140 | `BTC 0.0100` with average buy price and current price 67140.000000 (value 671.4000),
|
|---|
| 141 | the unchanged `ETH 0.5000` (average 3500, current 3520, unrealised P/L +10.0000),
|
|---|
| 142 | `Cash available : 7578.6000 USD` (= 8250.00 − 671.40), portfolio value 2431.4000 and
|
|---|
| 143 | net worth 10010.0000 USD. The `Reserved` column is 0.0000 on both rows — a buy never
|
|---|
| 144 | reserves anything.
|
|---|
| 145 |
|
|---|
| 146 | 
|
|---|
| 147 |
|
|---|
| 148 | ### Alternate flow 6a — insufficient funds
|
|---|
| 149 |
|
|---|
| 150 | User `charlie` (seed data: 2500.00 USD available, no crypto) chooses `[4]`, picks
|
|---|
| 151 | market `2` (BTC, 67140.000000) and enters quantity `1`. Go computes the notional
|
|---|
| 152 | 67140.00. In the transaction, statement (a) inserts the `open` order and statement (b)
|
|---|
| 153 | `SELECT available_balance FROM users WHERE id = $1 FOR UPDATE` returns 2500.00, which
|
|---|
| 154 | is less than the notional. `PlaceOrder` prints
|
|---|
| 155 | `Insufficient funds: need 67140.0000, have 2500.0000` and returns without running
|
|---|
| 156 | (c)–(g); the deferred `tx.Rollback()` undoes statement (a), so no order, no ledger
|
|---|
| 157 | entry and no balance change is left behind (after this run charlie has no row in
|
|---|
| 158 | `orders` and still 2500.00 USD available). The authenticated menu is shown again.
|
|---|
| 159 |
|
|---|
| 160 | 
|
|---|
| 161 |
|
|---|
| 162 | ### Alternate flow 3a — number not in the list
|
|---|
| 163 |
|
|---|
| 164 | If in step 3 the Trader enters something that is not a number from 1 to the number of
|
|---|
| 165 | listed markets, `pickNumber` prints `Invalid choice, enter a number from 1 to 5.`, no
|
|---|
| 166 | further SQL is run and the authenticated menu is shown again (the same check is shown
|
|---|
| 167 | in [UseCase0007](UseCase0007Implementation.md), alternate flow 12a). Likewise, a
|
|---|
| 168 | quantity that is not a positive number in step 5 prints `Invalid quantity.` before any
|
|---|
| 169 | transaction is opened.
|
|---|