Changeset ef1c1c7 for docs/P4-Prototype/UseCase0004Implementation.md
- Timestamp:
- 09/24/26 17:43:19 (5 days ago)
- Branches:
- main
- Children:
- 0cee8ec
- Parents:
- a531b45
- File:
-
- 1 edited
-
docs/P4-Prototype/UseCase0004Implementation.md (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
docs/P4-Prototype/UseCase0004Implementation.md
ra531b45 ref1c1c7 1 # Use-case 0004 Implementation — Buy1 # Use-case 0004 Implementation - Place market BUY order 2 2 3 **Initiating actor:** Trader . **Source file:** `server/trade.go`, function `PlaceOrder(s, "buy")`.3 **Initiating actor:** Trader 4 4 5 ## Scenario (implemented) 5 **Other actors:** Market Simulator (indirect — supplies the current price via `market_trades`). 6 6 7 1. **User** chooses `[4] Place market BUY order`. 8 2. **System** lists markets with their latest price (same SQL as UC0006 — via the `v_latest_prices` view). 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. 9 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). 10 23 11 3. **User** enters `BTC`. 12 4. **System** resolves the market and fetches the latest price: 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`): 13 38 14 39 ```sql 15 SELECT m.id, c.id, c.symbol, m.quote_currency 40 SELECT m.id, c.id, c.symbol, m.quote_currency, 41 COALESCE(lp.price, 0) AS price 16 42 FROM markets m 17 JOIN crypto cON c.id = m.crypto_id18 WHERE upper(c.symbol) = upper($1) AND m.is_active;19 20 SELECT price FROM v_latest_prices WHERE market_id = $2;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 21 47 ``` 22 48 23 5. **User** enters quantity `0.01`. 24 6. **System** executes a single database transaction — *all or nothing*: 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: 25 72 26 73 ```sql 27 74 BEGIN; 28 75 29 -- (a) record the order as 'open' — no trade has happened yet 30 INSERT INTO orders31 (user_id, market_id, side, type, status, quantity, price)32 VALUES33 ($1, $2, 'buy', 'market', 'open', $3, $4)34 RETURNING id;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; 35 82 36 -- (b) lock and check the user balance 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. 37 85 SELECT available_balance FROM users WHERE id = $1 FOR UPDATE; 38 86 39 -- (c) move cash from available to invested. A buy never reserves crypto 40 -- the way a sell does (see UC0005) — it only ever adds to the 41 -- position, so there is nothing to commit on the holdings side 42 -- before settling. 87 -- (c) move the notional from available to invested cash. 88 -- $1 = notional (671.40), $2 = user id. 43 89 UPDATE users 44 SET available_balance = available_balance - $notional,45 invested_balance = invested_balance + $notional,46 updated_at = now()47 WHERE id = $1;90 SET available_balance = available_balance - $1, 91 invested_balance = invested_balance + $1, 92 updated_at = now() 93 WHERE id = $2; 48 94 49 -- (d) upsert the holding, recomputing the weighted-average entry price50 -- in one statement. Every SET expression sees the pre-update row, so51 -- holdings.quantity belowis still the old quantity.52 -- reserved_quantity is untouched by a buy and defaults to 0.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). 53 99 INSERT INTO holdings (user_id, crypto_id, quantity, avg_price, updated_at) 54 VALUES ($1, $c, $3, $4, now())55 ON CONFLICT (user_id, crypto_id) DO UPDATE56 SET avg_price = (holdings.quantity * holdings.avg_price57 + EXCLUDED.quantity * EXCLUDED.avg_price)58 / (holdings.quantity + EXCLUDED.quantity),59 quantity = holdings.quantity + EXCLUDED.quantity,60 updated_at = now();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(); 61 107 62 -- (e) ledger entry 63 INSERT INTO transactions 64 (user_id, type, amount, currency, related_order, description) 65 VALUES 66 ($1, 'buy', -$notional, 'USD', $orderId, 'Market buy ...'); 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); 67 112 68 -- (f) record the resulting market trade 69 INSERT INTO market_trades 70 (market_id, executed_at, price, quantity, side, source) 71 VALUES 72 ($2, now(), $4, $3, 'buy', 'user'); 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'); 73 117 74 -- (g) settle the order itself — it has now actually been filled 75 UPDATE orders SET status = 'executed', executed_at = now() WHERE id = $ orderId;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; 76 120 77 121 COMMIT; 78 122 ``` 79 123 80 7. **System** prints: `Order executed: buy 0.0100 BTC @ 67140.000000 (notional 671.4000 USD)`. 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. 81 127 82  128 7. **System** confirms 129 `Order executed: buy 0.0100 BTC @ 67140.000000 (notional 671.4000 USD)` and shows 130 the authenticated menu again. 83 131 84 ## Verified run (from actual prototype execution) 132 The screenshot shows steps 5–7: the entered quantity, the confirmation and the menu. 85 133 86 Re-run 2026-09-16 against PostgreSQL 16 (`bp_database` on `localhost:5433`) with freshly loaded seed data: 134  87 135 88 - **Before:** alice.available_balance = 8250.00, portfolio = { ETH: 0.5000, reserved 0.0000 }. 89 - **Command:** `buy 0.01 BTC`. 90 - **After:** alice.available_balance = 7578.60 (= 8250 − 671.40), portfolio = { BTC: 0.0100 @ 67140 (reserved 0.0000), ETH: 0.5000 @ 3500 (reserved 0.0000) }, net worth = 10010.00 USD (the +10 is the ETH unrealised P/L from the price moving from 3500 → 3520). A buy never sets `reserved_quantity`, so it reads 0 on every row here. 136 ### Verification — portfolio after the buy 91 137 92 ## Failure path — insufficient funds 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. 93 145 94 If `available_balance < notional`, the `defer tx.Rollback()` in `server/trade.go` reverts every statement above and the user sees: 146  95 147 96 ``` 97 Insufficient funds: need X, have Y 98 ``` 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.
Note:
See TracChangeset
for help on using the changeset viewer.
