source: docs/P4-Prototype/UseCase0004Implementation.md

main
Last change on this file was ef1c1c7, checked in by Stefan <trsunovstefan@…>, 5 days ago

Wiki docs, phase 6 and phase 7 added

  • Property mode set to 100644
File size: 8.2 KB
Line 
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
7A logged-in Trader buys a crypto asset at the current market price. The Trader never
8types a symbol or an identifier: the system lists the active markets with their last
9price, numbered, and the Trader picks one by its number and then enters only the
10quantity. The system checks that the Trader has enough available cash for
11quantity × price and then, in one database transaction, records the order, moves the
12cash from available to invested, adds the crypto to the Trader's holding (recomputing
13the weighted-average entry price), writes a ledger entry and a market trade, and marks
14the 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
16back.
17
18Original use-case description (P3): [UseCase0004](../P3-UseCaseModel/UseCase0004.md).
19Implementation: [`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
24All statements run on the `project` schema: the connection sets
25`search_path=project,public` (`server/db/db.go`), so `orders` means `project.orders`.
26The SQL below is copied from the Go code; only the Go source indentation is removed,
27a `;` is added after each statement of the transaction, and `--` comments say what
28each `$n` placeholder is bound to.
29
30The run shown is user `alice` on the seed data (available 8250.00 USD, holding
310.5 ETH bought at 3500), buying 0.01 BTC.
32
33## Scenario
34
351. **Trader** chooses `[4] Place market BUY order` in the authenticated menu (types `4`).
362. **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 ![UC0004 steps 1-2: Trader chooses BUY, system lists the markets](screenshots/uc0004_1_2_markets.png)
54
553. **Trader** picks the market by its number in the list: `2` (BTC/USD).
564. **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 ![UC0004 steps 3-4: Trader picks market #2 (BTC), system shows the price](screenshots/uc0004_3_4_price.png)
67
685. **Trader** enters the quantity `0.01`.
696. **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
1287. **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 ![UC0004 steps 5-7: quantity entered, order executed](screenshots/uc0004_5_7_executed.png)
135
136### Verification — portfolio after the buy
137
138Right 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),
141the 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
143net worth 10010.0000 USD. The `Reserved` column is 0.0000 on both rows — a buy never
144reserves anything.
145
146![UC0004 verification: portfolio after the buy](screenshots/uc0004_verify_portfolio.png)
147
148### Alternate flow 6a — insufficient funds
149
150User `charlie` (seed data: 2500.00 USD available, no crypto) chooses `[4]`, picks
151market `2` (BTC, 67140.000000) and enters quantity `1`. Go computes the notional
15267140.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
154is 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
157entry 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![UC0004 alternate flow: insufficient funds](screenshots/uc0004_6a_insufficient.png)
161
162### Alternate flow 3a — number not in the list
163
164If in step 3 the Trader enters something that is not a number from 1 to the number of
165listed markets, `pickNumber` prints `Invalid choice, enter a number from 1 to 5.`, no
166further SQL is run and the authenticated menu is shown again (the same check is shown
167in [UseCase0007](UseCase0007Implementation.md), alternate flow 12a). Likewise, a
168quantity that is not a positive number in step 5 prints `Invalid quantity.` before any
169transaction is opened.
Note: See TracBrowser for help on using the repository browser.