Ignore:
Timestamp:
09/24/26 17:43:19 (5 days ago)
Author:
Stefan <trsunovstefan@…>
Branches:
main
Children:
0cee8ec
Parents:
a531b45
Message:

Wiki docs, phase 6 and phase 7 added

File:
1 edited

Legend:

Unmodified
Added
Removed
  • docs/P4-Prototype/UseCase0004Implementation.md

    ra531b45 ref1c1c7  
    1 # Use-case 0004 Implementation — Buy
     1# Use-case 0004 Implementation - Place market BUY order
    22
    3 **Initiating actor:** Trader. **Source file:** `server/trade.go`, function `PlaceOrder(s, "buy")`.
     3**Initiating actor:** Trader
    44
    5 ## Scenario (implemented)
     5**Other actors:** Market Simulator (indirect — supplies the current price via `market_trades`).
    66
    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).
     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.
    917
     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).
    1023
    11 3. **User** enters `BTC`.
    12 4. **System** resolves the market and fetches the latest price:
     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`):
    1338
    1439   ```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
    1642     FROM markets m
    17      JOIN crypto c ON c.id = m.crypto_id
    18     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
    2147   ```
    2248
    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   ![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:
    2572
    2673   ```sql
    2774   BEGIN;
    2875
    29    -- (a) record the order as 'open' — no trade has happened yet
    30    INSERT INTO orders
    31        (user_id, market_id, side, type, status, quantity, price)
    32    VALUES
    33        ($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;
    3582
    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.
    3785   SELECT available_balance FROM users WHERE id = $1 FOR UPDATE;
    3886
    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.
    4389   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;
    4894
    49    -- (d) upsert the holding, recomputing the weighted-average entry price
    50    --     in one statement. Every SET expression sees the pre-update row, so
    51    --     holdings.quantity below is 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).
    5399   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 UPDATE
    56       SET avg_price  = (holdings.quantity * holdings.avg_price
    57                          + 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();
    61107
    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);
    67112
    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');
    73117
    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;
    76120
    77121   COMMIT;
    78122   ```
    79123
    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.
    81127
    82    ![Market list, then a filled BUY order](screenshots/uc0004_buy.png)
     1287. **System** confirms
     129   `Order executed: buy 0.0100 BTC @ 67140.000000 (notional 671.4000 USD)` and shows
     130   the authenticated menu again.
    83131
    84 ## Verified run (from actual prototype execution)
     132   The screenshot shows steps 5–7: the entered quantity, the confirmation and the menu.
    85133
    86 Re-run 2026-09-16 against PostgreSQL 16 (`bp_database` on `localhost:5433`) with freshly loaded seed data:
     134   ![UC0004 steps 5-7: quantity entered, order executed](screenshots/uc0004_5_7_executed.png)
    87135
    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
    91137
    92 ## Failure path — insufficient funds
     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.
    93145
    94 If `available_balance < notional`, the `defer tx.Rollback()` in `server/trade.go` reverts every statement above and the user sees:
     146![UC0004 verification: portfolio after the buy](screenshots/uc0004_verify_portfolio.png)
    95147
    96 ```
    97 Insufficient funds: need X, have Y
    98 ```
     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 TracChangeset for help on using the changeset viewer.