Changes between Initial Version and Version 1 of UseCase0005Implementation


Ignore:
Timestamp:
09/24/26 14:03:07 (4 days ago)
Author:
231285
Comment:

--

Legend:

Unmodified
Added
Removed
Modified
  • UseCase0005Implementation

    v1 v1  
     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): [wiki:UseCase0005].
     18Implementation: `server/trade.go`, function
     19`PlaceOrder(s, "sell")`, which calls `ChooseHolding`, `pickNumber` and `LatestPrice`
     20from `server/market.go` (the code is shown at the end of this page).
     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[wiki:UseCase0004Implementation UseCase0004]: 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
     45 1. '''Trader''' chooses `[5] Place market SELL order` in the authenticated menu (types `5`).
     46 2. '''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{{{
     52SELECT 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
     64For 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
     66market id and crypto id in memory; the Trader only types the list number. (If the
     67query returns no row, the system prints `you hold no crypto that is free to sell`
     68and the use-case ends.)
     69
     70[[Image(uc0005_1_2_holdings.png)]]
     71
     72 3. '''Trader''' picks the holding by its number in the list: `2` (ETH).
     73 4. '''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{{{
     77SELECT price FROM v_latest_prices WHERE market_id = $1
     78}}}
     79
     80It prints `Latest price for ETH/USD = 3520.000000` and asks `Quantity:`.
     81
     82[[Image(uc0005_3_4_price.png)]]
     83
     84 5. '''Trader''' enters the quantity `0.2`.
     85 6. '''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{{{
     92BEGIN;
     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.
     97INSERT 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.
     105SELECT 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.
     110UPDATE 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).
     117UPDATE 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.
     125UPDATE 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'.
     133INSERT 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').
     138INSERT 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.
     142UPDATE orders SET status = 'executed', executed_at = now() WHERE id = $1;
     143
     144COMMIT;
     145}}}
     146
     147 7. '''System''' confirms
     148    `Order executed: sell 0.2000 ETH @ 3520.000000 (notional 704.0000 USD)` and shows
     149    the authenticated menu again.
     150
     151The screenshot shows steps 5–7: the entered quantity, the confirmation and the menu.
     152
     153[[Image(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[[Image(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{{{
     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}}}
     261
     262== Source code ==
     263
     264`server/trade.go` — `PlaceOrder` (buy and sell):
     265
     266{{{
     267// PlaceOrder - UC0004 (buy) / UC0005 (sell)
     268// Market order that executes immediately against the latest price.
     269// Runs inside a single database transaction so the orders, holdings,
     270// users.balance and transactions tables always agree.
     271//
     272// The order still passes through 'open' before 'executed'. Placing it
     273// reserves whatever it commits — on a sell, the crypto being sold, tracked in
     274// holdings.reserved_quantity — before anything is actually moved, so a
     275// second order against the same holding can never be granted the same units
     276// twice. Because only market orders are implemented, reserve and settle
     277// happen inside this one transaction rather than across two commits; a
     278// future limit-order matcher would split them into a second transaction
     279// later, without needing a schema change.
     280func PlaceOrder(s *Session, side string) {
     281        if side != "buy" && side != "sell" {
     282                fmt.Println("Invalid side.")
     283                return
     284        }
     285        fmt.Printf("\n-- Place market %s order --\n", side)
     286
     287        // buy: any market; sell: only what the user holds and can still sell
     288        var m *Market
     289        var err error
     290        if side == "buy" {
     291                m, err = ChooseMarket()
     292        } else {
     293                m, err = ChooseHolding(s)
     294        }
     295        if err != nil {
     296                fmt.Println(err)
     297                return
     298        }
     299        price, err := LatestPrice(m.ID)
     300        if err != nil {
     301                fmt.Println(err)
     302                return
     303        }
     304        fmt.Printf("Latest price for %s/%s = %.6f\n", m.Symbol, m.Quote, price)
     305
     306        qtyStr := prompt("Quantity: ")
     307        qty, err := strconv.ParseFloat(qtyStr, 64)
     308        if err != nil || qty <= 0 {
     309                fmt.Println("Invalid quantity.")
     310                return
     311        }
     312        notional := qty * price
     313
     314        tx, err := db.DB.Begin()
     315        if err != nil {
     316                fmt.Println("Error:", err)
     317                return
     318        }
     319        defer tx.Rollback()
     320
     321        // 1. record the order as 'open' — no trade has happened yet.
     322        var orderID string
     323        err = tx.QueryRow(
     324                `INSERT INTO orders (user_id, market_id, side, type, status, quantity, price)
     325                 VALUES ($1, $2, $3, 'market', 'open', $4, $5)
     326                 RETURNING id`,
     327                s.UserID, m.ID, side, qty, price,
     328        ).Scan(&orderID)
     329        if err != nil {
     330                fmt.Println("Error creating order:", err)
     331                return
     332        }
     333
     334        if side == "buy" {
     335                // check balance
     336                var avail float64
     337                if err := tx.QueryRow(
     338                        `SELECT available_balance FROM users WHERE id = $1 FOR UPDATE`,
     339                        s.UserID).Scan(&avail); err != nil {
     340                        fmt.Println("Error:", err)
     341                        return
     342                }
     343                if avail < notional {
     344                        fmt.Printf("Insufficient funds: need %.4f, have %.4f\n", notional, avail)
     345                        return
     346                }
     347
     348                // debit balance
     349                if _, err := tx.Exec(
     350                        `UPDATE users
     351                            SET available_balance = available_balance - $1,
     352                                invested_balance  = invested_balance  + $1,
     353                                updated_at        = now()
     354                          WHERE id = $2`,
     355                        notional, s.UserID,
     356                ); err != nil {
     357                        fmt.Println("Error:", err)
     358                        return
     359                }
     360
     361                // a buy never reserves crypto, only ever adds it — upsert holding
     362                // with running weighted average
     363                if err := upsertHoldingOnBuy(tx, s.UserID, m.CryptoID, qty, price); err != nil {
     364                        fmt.Println("Error updating holding:", err)
     365                        return
     366                }
     367
     368                // ledger entry
     369                if _, err := tx.Exec(
     370                        `INSERT INTO transactions (user_id, type, amount, currency, related_order, description)
     371                         VALUES ($1, 'buy', $2, 'USD', $3, $4)`,
     372                        s.UserID, -notional, orderID,
     373                        fmt.Sprintf("Market buy %.4f %s @ %.6f", qty, m.Symbol, price),
     374                ); err != nil {
     375                        fmt.Println("Error:", err)
     376                        return
     377                }
     378        } else {
     379                // sell: lock the holding and check what is actually free to sell —
     380                // quantity minus whatever another open order has already reserved.
     381                var held, reserved, avgPrice float64
     382                err := tx.QueryRow(
     383                        `SELECT quantity, reserved_quantity, avg_price FROM holdings
     384                          WHERE user_id = $1 AND crypto_id = $2 FOR UPDATE`,
     385                        s.UserID, m.CryptoID,
     386                ).Scan(&held, &reserved, &avgPrice)
     387                if err != nil && err != sql.ErrNoRows {
     388                        fmt.Println("Error:", err)
     389                        return
     390                }
     391                available := held - reserved
     392                if err == sql.ErrNoRows || available < qty {
     393                        fmt.Printf("Insufficient holding: trying to sell %.4f, available %.4f (of %.4f held, %.4f reserved)\n",
     394                                qty, available, held, reserved)
     395                        return
     396                }
     397
     398                // reserve: committed to this order, not yet removed from the position.
     399                if _, err := tx.Exec(
     400                        `UPDATE holdings
     401                            SET reserved_quantity = reserved_quantity + $1,
     402                                updated_at        = now()
     403                          WHERE user_id = $2 AND crypto_id = $3`,
     404                        qty, s.UserID, m.CryptoID,
     405                ); err != nil {
     406                        fmt.Println("Error:", err)
     407                        return
     408                }
     409
     410                // settle: a market order fills immediately, so release the
     411                // reservation and remove the asset from the position in one step.
     412                if _, err := tx.Exec(
     413                        `UPDATE holdings
     414                            SET quantity          = quantity - $1,
     415                                reserved_quantity = reserved_quantity - $1,
     416                                updated_at        = now()
     417                          WHERE user_id = $2 AND crypto_id = $3`,
     418                        qty, s.UserID, m.CryptoID,
     419                ); err != nil {
     420                        fmt.Println("Error:", err)
     421                        return
     422                }
     423
     424                // credit balance; reduce invested by cost basis (avg_price * qty)
     425                costBasis := avgPrice * qty
     426                if _, err := tx.Exec(
     427                        `UPDATE users
     428                            SET available_balance = available_balance + $1,
     429                                invested_balance  = GREATEST(invested_balance - $2, 0),
     430                                updated_at        = now()
     431                          WHERE id = $3`,
     432                        notional, costBasis, s.UserID,
     433                ); err != nil {
     434                        fmt.Println("Error:", err)
     435                        return
     436                }
     437
     438                // ledger entry
     439                if _, err := tx.Exec(
     440                        `INSERT INTO transactions (user_id, type, amount, currency, related_order, description)
     441                         VALUES ($1, 'sell', $2, 'USD', $3, $4)`,
     442                        s.UserID, notional, orderID,
     443                        fmt.Sprintf("Market sell %.4f %s @ %.6f", qty, m.Symbol, price),
     444                ); err != nil {
     445                        fmt.Println("Error:", err)
     446                        return
     447                }
     448        }
     449
     450        // record the resulting market trade so the book reflects this fill
     451        if _, err := tx.Exec(
     452                `INSERT INTO market_trades (market_id, executed_at, price, quantity, side, source)
     453                 VALUES ($1, now(), $2, $3, $4, 'user')`,
     454                m.ID, price, qty, side,
     455        ); err != nil {
     456                fmt.Println("Error:", err)
     457                return
     458        }
     459
     460        // settle the order itself: it has now actually been filled.
     461        if _, err := tx.Exec(
     462                `UPDATE orders SET status = 'executed', executed_at = now() WHERE id = $1`,
     463                orderID,
     464        ); err != nil {
     465                fmt.Println("Error:", err)
     466                return
     467        }
     468
     469        if err := tx.Commit(); err != nil {
     470                fmt.Println("Commit error:", err)
     471                return
     472        }
     473        fmt.Printf("Order executed: %s %.4f %s @ %.6f (notional %.4f USD)\n",
     474                side, qty, m.Symbol, price, notional)
     475}
     476}}}
     477
     478`server/market.go` — `pickNumber`, `ChooseHolding` and `LatestPrice`:
     479
     480{{{
     481// pickNumber reads a 1-based choice from a list of n items.
     482func pickNumber(label string, n int) (int, error) {
     483        if n == 0 {
     484                return 0, fmt.Errorf("Nothing to choose from.")
     485        }
     486        k, err := strconv.Atoi(prompt(label))
     487        if err != nil || k < 1 || k > n {
     488                return 0, fmt.Errorf("Invalid choice, enter a number from 1 to %d.", n)
     489        }
     490        return k - 1, nil
     491}
     492
     493// ChooseHolding lists only the markets the user can sell in — cryptos they
     494// hold with some quantity still free (not reserved by an open sell order) —
     495// with how much is held and free, and lets them pick one by number.
     496func ChooseHolding(s *Session) (*Market, error) {
     497        rows, err := db.DB.Query(`
     498                SELECT m.id, c.id, c.symbol, m.quote_currency,
     499                       h.quantity, h.quantity - h.reserved_quantity AS free,
     500                       COALESCE(lp.price, 0) AS price
     501                  FROM holdings h
     502                  JOIN crypto  c ON c.id = h.crypto_id
     503                  JOIN markets m ON m.crypto_id = c.id AND m.is_active = true
     504                  LEFT JOIN v_latest_prices lp ON lp.market_id = m.id
     505                 WHERE h.user_id = $1
     506                   AND h.quantity - h.reserved_quantity > 0
     507                 ORDER BY c.symbol`, s.UserID)
     508        if err != nil {
     509                return nil, err
     510        }
     511        defer rows.Close()
     512
     513        fmt.Println()
     514        fmt.Printf("  %-4s  %-8s  %-5s  %12s  %12s  %15s\n", "#", "Symbol", "Quote", "Held", "Free to sell", "Last price")
     515        fmt.Println("  -------------------------------------------------------------------")
     516        var list []Market
     517        for rows.Next() {
     518                var m Market
     519                var held, free, price float64
     520                if err := rows.Scan(&m.ID, &m.CryptoID, &m.Symbol, &m.Quote, &held, &free, &price); err != nil {
     521                        return nil, err
     522                }
     523                list = append(list, m)
     524                fmt.Printf("  %-4d  %-8s  %-5s  %12.4f  %12.4f  %15.6f\n", len(list), m.Symbol, m.Quote, held, free, price)
     525        }
     526        if len(list) == 0 {
     527                return nil, fmt.Errorf("you hold no crypto that is free to sell")
     528        }
     529        k, err := pickNumber("Holding #: ", len(list))
     530        if err != nil {
     531                return nil, err
     532        }
     533        return &list[k], nil
     534}
     535
     536// LatestPrice returns the last traded price on a market.
     537func LatestPrice(marketID string) (float64, error) {
     538        var price float64
     539        err := db.DB.QueryRow(
     540                `SELECT price FROM v_latest_prices WHERE market_id = $1`, marketID,
     541        ).Scan(&price)
     542        if err == sql.ErrNoRows {
     543                return 0, fmt.Errorf("no trades yet for this market")
     544        }
     545        return price, err
     546}
     547}}}