source: docs/P4-Prototype/UseCase0005Implementation.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: 12.4 KB
Line 
1# Use-case 0005 Implementation - Place market SELL order
2
3**Initiating actor:** Trader
4
5**Other actors:** Market Simulator (indirect — supplies the current price).
6
7A logged-in Trader sells part or all of a holding at the current market price. The
8Trader never types a symbol: the system lists only the cryptos the Trader holds and can
9still sell (the quantity not already reserved by an open sell order), numbered, with
10how much is held and how much is free, and the Trader picks one by its number and
11enters the quantity. In one database transaction the system records the order,
12reserves the crypto being sold and settles it, credits the proceeds to the Trader's
13available cash while reducing the invested cash by the cost basis, writes a ledger
14entry and a market trade, and marks the order executed. Cost basis is preserved, so
15the realised P/L can be reconstructed from the ledger.
16
17Original use-case description (P3): [UseCase0005](../P3-UseCaseModel/UseCase0005.md).
18Implementation: [`server/trade.go`](../../server/trade.go), function
19`PlaceOrder(s, "sell")`, which calls `ChooseHolding`, `pickNumber` and `LatestPrice`
20from [`server/market.go`](../../server/market.go).
21
22All statements run on the `project` schema (the connection sets
23`search_path=project,public` in `server/db/db.go`). The SQL below is copied from the
24Go code; only the Go source indentation is removed, a `;` is added after each
25statement of the transaction, and `--` comments say what each `$n` placeholder is
26bound to.
27
28The run shown is user `alice` right after the buy of
29[UseCase0004](UseCase0004Implementation.md): 7578.60 USD available, holdings
300.01 BTC (bought at 67140) and 0.5 ETH (bought at 3500). She sells 0.2 ETH.
31
32## Reserve, then settle
33
34The crypto being sold is **reserved** (`holdings.reserved_quantity`) before it is
35removed from the position, and the sell check is against what is truly still free,
36`quantity - reserved_quantity`, not against the raw `quantity`, which would also count
37crypto already promised to another order. Because only market orders are implemented,
38an order settles in the same transaction it is placed in, so reserve and settle are two
39statements inside one commit; they stay logically distinct so that a future
40limit-order matcher, where an order would stay `open` until a *later* transaction fills
41it, needs a second transaction but no schema change.
42
43## Scenario
44
451. **Trader** chooses `[5] Place market SELL order` in the authenticated menu (types `5`).
462. **System** prints `-- Place market sell order --` and lists, numbered, only the
47 cryptos the Trader holds with some quantity still free to sell, with the quantity
48 held, the quantity free to sell and the last price (`ChooseHolding`;
49 `$1` = the logged-in user's id):
50
51 ```sql
52 SELECT m.id, c.id, c.symbol, m.quote_currency,
53 h.quantity, h.quantity - h.reserved_quantity AS free,
54 COALESCE(lp.price, 0) AS price
55 FROM holdings h
56 JOIN crypto c ON c.id = h.crypto_id
57 JOIN markets m ON m.crypto_id = c.id AND m.is_active = true
58 LEFT JOIN v_latest_prices lp ON lp.market_id = m.id
59 WHERE h.user_id = $1
60 AND h.quantity - h.reserved_quantity > 0
61 ORDER BY c.symbol
62 ```
63
64 For alice it prints `1 BTC USD 0.0100 0.0100 67140.000000` and
65 `2 ETH USD 0.5000 0.5000 3520.000000`, then asks `Holding #:`. Go keeps each row's
66 market id and crypto id in memory; the Trader only types the list number. (If the
67 query returns no row, the system prints `you hold no crypto that is free to sell`
68 and the use-case ends.)
69
70 ![UC0005 steps 1-2: Trader chooses SELL, system lists what they hold](screenshots/uc0005_1_2_holdings.png)
71
723. **Trader** picks the holding by its number in the list: `2` (ETH).
734. **System** takes the market id and crypto id of row 2 from the list and reads the
74 latest price of that market (`LatestPrice`; `$1` = the chosen market's id):
75
76 ```sql
77 SELECT price FROM v_latest_prices WHERE market_id = $1
78 ```
79
80 It prints `Latest price for ETH/USD = 3520.000000` and asks `Quantity:`.
81
82 ![UC0005 steps 3-4: Trader picks holding #2 (ETH), system shows the price](screenshots/uc0005_3_4_price.png)
83
845. **Trader** enters the quantity `0.2`.
856. **System** computes in Go notional = quantity × price = 0.2 × 3520 = 704.00 and
86 runs one database transaction; the statements are in exactly the order
87 `PlaceOrder` executes them for a sell. After statement (b) Go also computes the
88 cost basis = avg_price × quantity = 3500 × 0.2 = 700.00 from the locked holding row;
89 both values are passed to SQL as parameters.
90
91 ```sql
92 BEGIN;
93
94 -- (a) record the order as 'open' — no trade has happened yet.
95 -- $1 = user id, $2 = market id, $3 = side (the Go variable side = 'sell'),
96 -- $4 = quantity (0.2), $5 = price (3520); the returned id is kept in Go.
97 INSERT INTO orders (user_id, market_id, side, type, status, quantity, price)
98 VALUES ($1, $2, $3, 'market', 'open', $4, $5)
99 RETURNING id;
100
101 -- (b) lock the holding row and read what is held, what is already reserved and
102 -- the average entry price. $1 = user id, $2 = crypto id.
103 -- Go computes available = quantity - reserved_quantity (0.5 - 0 = 0.5);
104 -- if there is no row or available < quantity -> alternate flow 5a.
105 SELECT quantity, reserved_quantity, avg_price FROM holdings
106 WHERE user_id = $1 AND crypto_id = $2 FOR UPDATE;
107
108 -- (c) reserve: committed to this order, not yet removed from the position.
109 -- $1 = quantity (0.2), $2 = user id, $3 = crypto id.
110 UPDATE holdings
111 SET reserved_quantity = reserved_quantity + $1,
112 updated_at = now()
113 WHERE user_id = $2 AND crypto_id = $3;
114
115 -- (d) settle: a market order fills immediately, so release the reservation and
116 -- remove the asset from the position in one step. Same parameters as (c).
117 UPDATE holdings
118 SET quantity = quantity - $1,
119 reserved_quantity = reserved_quantity - $1,
120 updated_at = now()
121 WHERE user_id = $2 AND crypto_id = $3;
122
123 -- (e) credit the proceeds; reduce invested cash by the cost basis.
124 -- $1 = notional (704.00), $2 = cost basis (700.00), $3 = user id.
125 UPDATE users
126 SET available_balance = available_balance + $1,
127 invested_balance = GREATEST(invested_balance - $2, 0),
128 updated_at = now()
129 WHERE id = $3;
130
131 -- (f) ledger entry. $1 = user id, $2 = notional (704.00), $3 = order id from (a),
132 -- $4 = description built in Go: 'Market sell 0.2000 ETH @ 3520.000000'.
133 INSERT INTO transactions (user_id, type, amount, currency, related_order, description)
134 VALUES ($1, 'sell', $2, 'USD', $3, $4);
135
136 -- (g) record the resulting market trade.
137 -- $1 = market id, $2 = price, $3 = quantity, $4 = side ('sell').
138 INSERT INTO market_trades (market_id, executed_at, price, quantity, side, source)
139 VALUES ($1, now(), $2, $3, $4, 'user');
140
141 -- (h) settle the order itself — it has now actually been filled. $1 = order id.
142 UPDATE orders SET status = 'executed', executed_at = now() WHERE id = $1;
143
144 COMMIT;
145 ```
146
1477. **System** confirms
148 `Order executed: sell 0.2000 ETH @ 3520.000000 (notional 704.0000 USD)` and shows
149 the authenticated menu again.
150
151 The screenshot shows steps 5–7: the entered quantity, the confirmation and the menu.
152
153 ![UC0005 steps 5-7: quantity entered, order executed](screenshots/uc0005_5_7_executed.png)
154
155After this run the database holds for alice: ETH `quantity` 0.3000 with
156`reserved_quantity` 0.0000; `available_balance` 8282.60 (= 7578.60 + 704.00) and
157`invested_balance` 1721.40 (= 2421.40 − 700.00); a `sell` row in `transactions` with
158amount 704.0000 and description `Market sell 0.2000 ETH @ 3520.000000`; and the order
159with status `executed`. The realised P/L of this sell is notional − cost basis =
160704.00 − 700.00 = +4.00 USD.
161
162### Alternate flow 5a — insufficient holding
163
164Right after the sell above, alice chooses `[5]` again. The list from step 2 now shows
165`2 ETH USD 0.3000 0.3000 3520.000000`. She picks `2` (ETH) and enters quantity `5`.
166In the transaction, statement (a) inserts the `open` order and statement (b) returns
167quantity 0.3000 and reserved_quantity 0.0000, so available = 0.3 < 5. `PlaceOrder`
168prints
169
170```
171Insufficient holding: trying to sell 5.0000, available 0.3000 (of 0.3000 held, 0.0000 reserved)
172```
173
174and returns without running (c)–(h); the deferred `tx.Rollback()` undoes statement (a)
175as well, so no order, no reservation and no ledger entry is left behind. The
176authenticated menu is shown again. The same message is printed if the holding row no
177longer exists (for example because it was sold out from another session after the
178list was shown).
179
180![UC0005 alternate flow 5a: selling more than is held](screenshots/uc0005_5a_insufficient.png)
181
182## Reserve and settle, step by step
183
184The CLI reserves and settles inside one transaction, so `reserved_quantity` is never
185nonzero *outside* a transaction. The intermediate state is shown by running statements
186(c) and (d) by hand in one `psql` transaction (which sees its own uncommitted
187writes) against alice's ETH holding after the scenario above (0.3 ETH), for a sell of
1880.1, and rolling back at the end so nothing is changed. Literal values replace the
189`$n` parameters; `:alice` and `:eth` are psql variables for
190`(SELECT id FROM users WHERE username = 'alice')` and
191`(SELECT id FROM crypto WHERE symbol = 'ETH')`:
192
193```sql
194BEGIN;
195SELECT quantity, reserved_quantity, quantity - reserved_quantity AS available
196 FROM holdings WHERE user_id = :alice AND crypto_id = :eth;
197-- quantity | reserved_quantity | available
198-- ----------+-------------------+-----------
199-- 0.3000 | 0.0000 | 0.3000
200
201-- (c) reserve 0.1: the order is placed, no trade has happened yet
202UPDATE holdings SET reserved_quantity = reserved_quantity + 0.1, updated_at = now()
203 WHERE user_id = :alice AND crypto_id = :eth;
204SELECT quantity, reserved_quantity, quantity - reserved_quantity AS available
205 FROM holdings WHERE user_id = :alice AND crypto_id = :eth;
206-- quantity | reserved_quantity | available
207-- ----------+-------------------+-----------
208-- 0.3000 | 0.1000 | 0.2000
209
210-- (d) settle: the reservation is released and the asset removed
211UPDATE holdings SET quantity = quantity - 0.1, reserved_quantity = reserved_quantity - 0.1, updated_at = now()
212 WHERE user_id = :alice AND crypto_id = :eth;
213SELECT quantity, reserved_quantity, quantity - reserved_quantity AS available
214 FROM holdings WHERE user_id = :alice AND crypto_id = :eth;
215-- quantity | reserved_quantity | available
216-- ----------+-------------------+-----------
217-- 0.2000 | 0.0000 | 0.2000
218ROLLBACK;
219```
220
221The middle state is what every other connection would see for as long as an order
222stayed `open` once limit orders exist: 0.1 ETH still owned but no longer free to sell.
223
224## Two concurrent sells
225
226A Trader must not be able to sell the same units twice from two sessions at once. Both
227sessions may have listed the holding as free (step 2 runs outside the transaction), so
228the protection is statement (b): `SELECT ... FOR UPDATE` locks the holding row, and a
229second transaction that reaches (b) waits until the first one commits, then reads the
230already reduced `quantity` before deciding.
231
232This was checked with two `psql` sessions running statements (b)–(d) against alice's
2330.3 ETH. Session A locked the row, reserved and settled 0.2 ETH and committed after a
2343-second pause; session B asked for the lock one second after A had taken it:
235
236```
237A: SELECT ... FOR UPDATE -> quantity 0.3000, reserved_quantity 0.0000
238A: reserve 0.2, settle 0.2, pg_sleep(3)
239B: 11:43:54 SELECT ... FOR UPDATE -- blocks, A holds the row lock
240A: 11:43:56 COMMIT
241B: 11:43:56 lock granted -> quantity 0.1000, reserved_quantity 0.0000
242```
243
244Session B was blocked for the two seconds until A committed and then saw only
2450.1 ETH, so a second 0.2 ETH sell in B takes alternate flow 5a
246(`available 0.1000`) instead of selling units that no longer exist. (B was rolled
247back and alice's holding was restored to 0.3 ETH after the check.)
248
249## The constraint holds even if the application code did not
250
251`schema_creation.sql` declares
252`CHECK (reserved_quantity >= 0 AND reserved_quantity <= quantity)` on
253`holdings.reserved_quantity`, so an inconsistent reservation is impossible at the
254database level, independently of `trade.go` (run inside a transaction that was
255rolled back):
256
257```
258UPDATE holdings SET reserved_quantity = quantity + 1 WHERE user_id = :alice AND crypto_id = :eth;
259ERROR: new row for relation "holdings" violates check constraint "holdings_check"
260```
Note: See TracBrowser for help on using the repository browser.