source: server/trade.go@ b715712

main
Last change on this file since b715712 was b715712, checked in by Stefan <trsunovstefan@…>, 8 weeks ago

Add the server side and configuration

  • Property mode set to 100644
File size: 5.6 KB
Line 
1package main
2
3import (
4 "database/sql"
5 "fmt"
6 "strconv"
7
8 "bp_project/server/db"
9)
10
11// PlaceOrder - UC0004 (buy) / UC0005 (sell)
12// Market order that executes immediately against the latest price.
13// Runs inside a single database transaction so the orders, holdings,
14// users.balance and transactions tables always agree.
15func PlaceOrder(s *Session, side string) {
16 if side != "buy" && side != "sell" {
17 fmt.Println("Invalid side.")
18 return
19 }
20 fmt.Printf("\n-- Place market %s order --\n", side)
21
22 m, err := ChooseMarket()
23 if err != nil {
24 fmt.Println(err)
25 return
26 }
27 price, err := LatestPrice(m.ID)
28 if err != nil {
29 fmt.Println(err)
30 return
31 }
32 fmt.Printf("Latest price for %s/%s = %.6f\n", m.Symbol, m.Quote, price)
33
34 qtyStr := prompt("Quantity: ")
35 qty, err := strconv.ParseFloat(qtyStr, 64)
36 if err != nil || qty <= 0 {
37 fmt.Println("Invalid quantity.")
38 return
39 }
40 notional := qty * price
41
42 tx, err := db.DB.Begin()
43 if err != nil {
44 fmt.Println("Error:", err)
45 return
46 }
47 defer tx.Rollback()
48
49 // 1. create the order (status='executed' since we fill immediately)
50 var orderID string
51 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())
54 RETURNING id`,
55 s.UserID, m.ID, side, qty, price,
56 ).Scan(&orderID)
57 if err != nil {
58 fmt.Println("Error creating order:", err)
59 return
60 }
61
62 if side == "buy" {
63 // check balance
64 var avail float64
65 if err := tx.QueryRow(
66 `SELECT available_balance FROM users WHERE id = $1 FOR UPDATE`,
67 s.UserID).Scan(&avail); err != nil {
68 fmt.Println("Error:", err)
69 return
70 }
71 if avail < notional {
72 fmt.Printf("Insufficient funds: need %.4f, have %.4f\n", notional, avail)
73 return
74 }
75
76 // debit balance
77 if _, err := tx.Exec(
78 `UPDATE users
79 SET available_balance = available_balance - $1,
80 invested_balance = invested_balance + $1,
81 updated_at = now()
82 WHERE id = $2`,
83 notional, s.UserID,
84 ); err != nil {
85 fmt.Println("Error:", err)
86 return
87 }
88
89 // upsert holding with running weighted average
90 if err := upsertHoldingOnBuy(tx, s.UserID, m.CryptoID, qty, price); err != nil {
91 fmt.Println("Error updating holding:", err)
92 return
93 }
94
95 // ledger entry
96 if _, err := tx.Exec(
97 `INSERT INTO transactions (user_id, type, amount, currency, related_order, description)
98 VALUES ($1, 'buy', $2, 'USD', $3, $4)`,
99 s.UserID, -notional, orderID,
100 fmt.Sprintf("Market buy %.4f %s @ %.6f", qty, m.Symbol, price),
101 ); err != nil {
102 fmt.Println("Error:", err)
103 return
104 }
105 } else {
106 // sell: check holding
107 var held, avgPrice float64
108 err := tx.QueryRow(
109 `SELECT quantity, avg_price FROM holdings
110 WHERE user_id = $1 AND crypto_id = $2 FOR UPDATE`,
111 s.UserID, m.CryptoID,
112 ).Scan(&held, &avgPrice)
113 if err != nil && err != sql.ErrNoRows {
114 fmt.Println("Error:", err)
115 return
116 }
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
123 if _, err := tx.Exec(
124 `UPDATE holdings
125 SET quantity = quantity - $1,
126 updated_at = now()
127 WHERE user_id = $2 AND crypto_id = $3`,
128 qty, s.UserID, m.CryptoID,
129 ); err != nil {
130 fmt.Println("Error:", err)
131 return
132 }
133
134 // credit balance; reduce invested by cost basis (avg_price * qty)
135 costBasis := avgPrice * qty
136 if _, err := tx.Exec(
137 `UPDATE users
138 SET available_balance = available_balance + $1,
139 invested_balance = GREATEST(invested_balance - $2, 0),
140 updated_at = now()
141 WHERE id = $3`,
142 notional, costBasis, s.UserID,
143 ); err != nil {
144 fmt.Println("Error:", err)
145 return
146 }
147
148 // ledger entry
149 if _, err := tx.Exec(
150 `INSERT INTO transactions (user_id, type, amount, currency, related_order, description)
151 VALUES ($1, 'sell', $2, 'USD', $3, $4)`,
152 s.UserID, notional, orderID,
153 fmt.Sprintf("Market sell %.4f %s @ %.6f", qty, m.Symbol, price),
154 ); err != nil {
155 fmt.Println("Error:", err)
156 return
157 }
158 }
159
160 // record the resulting market trade so the book reflects this fill
161 if _, err := tx.Exec(
162 `INSERT INTO market_trades (market_id, executed_at, price, quantity, side, source)
163 VALUES ($1, now(), $2, $3, $4, 'user')`,
164 m.ID, price, qty, side,
165 ); err != nil {
166 fmt.Println("Error:", err)
167 return
168 }
169
170 if err := tx.Commit(); err != nil {
171 fmt.Println("Commit error:", err)
172 return
173 }
174 fmt.Printf("Order executed: %s %.4f %s @ %.6f (notional %.4f USD)\n",
175 side, qty, m.Symbol, price, notional)
176}
177
178// upsertHoldingOnBuy creates or updates a holding using running weighted-average price.
179//
180// This is a single statement that relies on UNIQUE (user_id, crypto_id): the new
181// weighted average is recomputed by the database in numeric arithmetic rather
182// than in Go float64, and no separate SELECT ... FOR UPDATE round-trip is
183// needed because ON CONFLICT DO UPDATE locks the conflicting row itself.
184// Every SET expression sees the pre-update row, so `holdings.quantity` below is
185// still the old quantity while the average is being computed.
186func upsertHoldingOnBuy(tx *sql.Tx, userID, cryptoID string, qty, price float64) error {
187 _, err := tx.Exec(
188 `INSERT INTO holdings (user_id, crypto_id, quantity, avg_price, updated_at)
189 VALUES ($1, $2, $3, $4, now())
190 ON CONFLICT (user_id, crypto_id) DO UPDATE
191 SET avg_price = (holdings.quantity * holdings.avg_price
192 + EXCLUDED.quantity * EXCLUDED.avg_price)
193 / (holdings.quantity + EXCLUDED.quantity),
194 quantity = holdings.quantity + EXCLUDED.quantity,
195 updated_at = now()`,
196 userID, cryptoID, qty, price,
197 )
198 return err
199}
Note: See TracBrowser for help on using the repository browser.