Changeset 9577c79 for server/trade.go
- Timestamp:
- 09/16/26 23:37:15 (13 days ago)
- Branches:
- main
- Children:
- 8b447ef
- Parents:
- df05838
- File:
-
- 1 edited
-
server/trade.go (modified) (5 diffs)
Legend:
- Unmodified
- Added
- Removed
-
server/trade.go
rdf05838 r9577c79 13 13 // Runs inside a single database transaction so the orders, holdings, 14 14 // users.balance and transactions tables always agree. 15 // 16 // The order still passes through 'open' before 'executed'. Placing it 17 // reserves whatever it commits — on a sell, the crypto being sold, tracked in 18 // holdings.reserved_quantity — before anything is actually moved, so a 19 // second order against the same holding can never be granted the same units 20 // twice. Because only market orders are implemented, reserve and settle 21 // happen inside this one transaction rather than across two commits; a 22 // future limit-order matcher would split them into a second transaction 23 // later, without needing a schema change. 15 24 func PlaceOrder(s *Session, side string) { 16 25 if side != "buy" && side != "sell" { … … 47 56 defer tx.Rollback() 48 57 49 // 1. create the order (status='executed' since we fill immediately)58 // 1. record the order as 'open' — no trade has happened yet. 50 59 var orderID string 51 60 err = tx.QueryRow( 52 `INSERT INTO orders (user_id, market_id, side, type, status, quantity, price , executed_at)53 VALUES ($1, $2, $3, 'market', ' executed', $4, $5, now())61 `INSERT INTO orders (user_id, market_id, side, type, status, quantity, price) 62 VALUES ($1, $2, $3, 'market', 'open', $4, $5) 54 63 RETURNING id`, 55 64 s.UserID, m.ID, side, qty, price, … … 87 96 } 88 97 89 // upsert holding with running weighted average 98 // a buy never reserves crypto, only ever adds it — upsert holding 99 // with running weighted average 90 100 if err := upsertHoldingOnBuy(tx, s.UserID, m.CryptoID, qty, price); err != nil { 91 101 fmt.Println("Error updating holding:", err) … … 104 114 } 105 115 } else { 106 // sell: check holding 107 var held, avgPrice float64 116 // sell: lock the holding and check what is actually free to sell — 117 // quantity minus whatever another open order has already reserved. 118 var held, reserved, avgPrice float64 108 119 err := tx.QueryRow( 109 `SELECT quantity, avg_price FROM holdings120 `SELECT quantity, reserved_quantity, avg_price FROM holdings 110 121 WHERE user_id = $1 AND crypto_id = $2 FOR UPDATE`, 111 122 s.UserID, m.CryptoID, 112 ).Scan(&held, & avgPrice)123 ).Scan(&held, &reserved, &avgPrice) 113 124 if err != nil && err != sql.ErrNoRows { 114 125 fmt.Println("Error:", err) 115 126 return 116 127 } 117 if err == sql.ErrNoRows || held < qty { 118 fmt.Printf("Insufficient holding: trying to sell %.4f, hold %.4f\n", qty, held) 119 return 120 } 121 122 // reduce holding 128 available := held - reserved 129 if err == sql.ErrNoRows || available < qty { 130 fmt.Printf("Insufficient holding: trying to sell %.4f, available %.4f (of %.4f held, %.4f reserved)\n", 131 qty, available, held, reserved) 132 return 133 } 134 135 // reserve: committed to this order, not yet removed from the position. 123 136 if _, err := tx.Exec( 124 137 `UPDATE holdings 125 SET quantity = quantity - $1, 126 updated_at = now() 138 SET reserved_quantity = reserved_quantity + $1, 139 updated_at = now() 140 WHERE user_id = $2 AND crypto_id = $3`, 141 qty, s.UserID, m.CryptoID, 142 ); err != nil { 143 fmt.Println("Error:", err) 144 return 145 } 146 147 // settle: a market order fills immediately, so release the 148 // reservation and remove the asset from the position in one step. 149 if _, err := tx.Exec( 150 `UPDATE holdings 151 SET quantity = quantity - $1, 152 reserved_quantity = reserved_quantity - $1, 153 updated_at = now() 127 154 WHERE user_id = $2 AND crypto_id = $3`, 128 155 qty, s.UserID, m.CryptoID, … … 163 190 VALUES ($1, now(), $2, $3, $4, 'user')`, 164 191 m.ID, price, qty, side, 192 ); err != nil { 193 fmt.Println("Error:", err) 194 return 195 } 196 197 // settle the order itself: it has now actually been filled. 198 if _, err := tx.Exec( 199 `UPDATE orders SET status = 'executed', executed_at = now() WHERE id = $1`, 200 orderID, 165 201 ); err != nil { 166 202 fmt.Println("Error:", err)
Note:
See TracChangeset
for help on using the changeset viewer.
