source: server/trade.go

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: 7.1 KB
Line 
1package main
2
3import (
4 "errors"
5 "fmt"
6 "strconv"
7 "strings"
8
9 "github.com/lib/pq"
10
11 "bp_project/server/db"
12)
13
14// PlaceOrder - UC0004 (buy) / UC0005 (sell)
15// Market or limit order. All the database work — checking free cash/crypto,
16// reserving it, recording the order, matching it against the order book and
17// filling the rest from the simulated market — is done by the P7 stored
18// function project.place_order in one call, so it is one atomic statement
19// and the P7 triggers keep orders, trades, holdings and balances consistent.
20func PlaceOrder(s *Session, side string) {
21 if side != "buy" && side != "sell" {
22 fmt.Println("Invalid side.")
23 return
24 }
25 fmt.Printf("\n-- Place %s order --\n", side)
26
27 // buy: any market; sell: only what the user holds and can still sell
28 var m *Market
29 var err error
30 if side == "buy" {
31 m, err = ChooseMarket()
32 } else {
33 m, err = ChooseHolding(s)
34 }
35 if err != nil {
36 fmt.Println(err)
37 return
38 }
39 price, err := LatestPrice(m.ID)
40 if err != nil {
41 fmt.Println(err)
42 return
43 }
44 fmt.Printf("Latest price for %s/%s = %.6f\n", m.Symbol, m.Quote, price)
45 printBookSide(m, side)
46
47 fmt.Println("[1] Market order (fills now at the best available price)")
48 fmt.Println("[2] Limit order (fills only at your price or better, otherwise waits in the order book)")
49 orderType := map[string]string{"1": "market", "2": "limit"}[prompt("> ")]
50 if orderType == "" {
51 fmt.Println("Unknown option.")
52 return
53 }
54
55 qty, err := strconv.ParseFloat(prompt("Quantity: "), 64)
56 if err != nil || qty <= 0 {
57 fmt.Println("Invalid quantity.")
58 return
59 }
60 var limit optFloat
61 if orderType == "limit" {
62 p, err := strconv.ParseFloat(prompt("Limit price: "), 64)
63 if err != nil || p <= 0 {
64 fmt.Println("Invalid price.")
65 return
66 }
67 limit = optFloat{p, true}
68 }
69
70 var orderID string
71 err = db.DB.QueryRow(
72 `SELECT place_order($1, $2, $3, $4, $5, $6)`,
73 s.UserID, m.ID, side, orderType, qty, limit.value(),
74 ).Scan(&orderID)
75 if err != nil {
76 fmt.Println(dbMessage(err))
77 return
78 }
79
80 var status string
81 var filled, remaining float64
82 var avg *float64
83 if err := db.DB.QueryRow(
84 `SELECT status, filled_quantity, remaining, avg_fill_price
85 FROM v_order_history WHERE order_id = $1`, orderID,
86 ).Scan(&status, &filled, &remaining, &avg); err != nil {
87 fmt.Println("Error:", err)
88 return
89 }
90 switch status {
91 case "executed":
92 fmt.Printf("Order executed: %s %.4f %s, average price %.6f\n", side, filled, m.Symbol, *avg)
93 case "partially_filled":
94 fmt.Printf("Order partially filled: %.4f %s at average %.6f, %.4f waiting in the order book\n",
95 filled, m.Symbol, *avg, remaining)
96 default:
97 fmt.Printf("Order placed in the order book: %s %.4f %s at %.6f\n", side, remaining, m.Symbol, limit.v)
98 }
99}
100
101// optFloat is an optional float parameter (NULL when not set).
102type optFloat struct {
103 v float64
104 ok bool
105}
106
107func (n optFloat) value() any {
108 if !n.ok {
109 return nil
110 }
111 return n.v
112}
113
114// dbMessage shows a rule the database refused (P7 raises check_violation
115// with a readable message) without the driver's prefix.
116func dbMessage(err error) string {
117 var pqErr *pq.Error
118 if errors.As(err, &pqErr) && pqErr.Code.Class() == "23" {
119 return "Rejected: " + pqErr.Message
120 }
121 return "Error: " + err.Error()
122}
123
124// printBookSide shows the best resting orders on the side this order would
125// trade against (asks for a buy, bids for a sell).
126func printBookSide(m *Market, side string) {
127 other, order := "sell", "price ASC"
128 if side == "sell" {
129 other, order = "buy", "price DESC"
130 }
131 rows, err := db.DB.Query(
132 `SELECT price, quantity, orders FROM v_order_book
133 WHERE market_id = $1 AND side = $2 ORDER BY `+order+` LIMIT 5`, m.ID, other)
134 if err != nil {
135 fmt.Println("Error:", err)
136 return
137 }
138 defer rows.Close()
139 label := map[string]string{"sell": "asks", "buy": "bids"}[other]
140 first := true
141 for rows.Next() {
142 var price, qty float64
143 var n int
144 if err := rows.Scan(&price, &qty, &n); err != nil {
145 fmt.Println("scan error:", err)
146 return
147 }
148 if first {
149 fmt.Printf("Order book %s (other users' limit orders):\n", label)
150 first = false
151 }
152 fmt.Printf(" %14.6f %12.4f (%d orders)\n", price, qty, n)
153 }
154 if first {
155 fmt.Printf("Order book has no %s - a market order fills from the simulated market.\n", label)
156 }
157}
158
159// ShowOrderBook - P7 view v_order_book for one market.
160func ShowOrderBook() {
161 m, err := ChooseMarket()
162 if err != nil {
163 fmt.Println(err)
164 return
165 }
166 rows, err := db.DB.Query(
167 `SELECT side, price, quantity, orders FROM v_order_book
168 WHERE market_id = $1
169 ORDER BY side DESC, price DESC`, m.ID)
170 if err != nil {
171 fmt.Println("Error:", err)
172 return
173 }
174 defer rows.Close()
175 fmt.Printf("\n Order book %s/%s\n", m.Symbol, m.Quote)
176 fmt.Printf(" %-5s %14s %12s %6s\n", "Side", "Price", "Quantity", "Orders")
177 fmt.Println(" " + strings.Repeat("-", 44))
178 empty := true
179 for rows.Next() {
180 var side string
181 var price, qty float64
182 var n int
183 if err := rows.Scan(&side, &price, &qty, &n); err != nil {
184 fmt.Println("scan error:", err)
185 return
186 }
187 fmt.Printf(" %-5s %14.6f %12.4f %6d\n", map[string]string{"sell": "ask", "buy": "bid"}[side], price, qty, n)
188 empty = false
189 }
190 if empty {
191 fmt.Println(" (no resting limit orders)")
192 }
193}
194
195// activeOrder is one row of the user's open orders list.
196type activeOrder struct {
197 id, symbol, side, typ, status string
198 qty, filled, remaining, price float64
199}
200
201// listMyOrders prints the user's active orders numbered 1..n (P7 view
202// v_active_orders) and returns them, so the user can pick one by number.
203func listMyOrders(s *Session) []activeOrder {
204 rows, err := db.DB.Query(
205 `SELECT order_id, symbol, side, type, status, quantity, filled_quantity, remaining, price
206 FROM v_active_orders WHERE user_id = $1 ORDER BY placed_at`, s.UserID)
207 if err != nil {
208 fmt.Println("Error:", err)
209 return nil
210 }
211 defer rows.Close()
212 var list []activeOrder
213 for rows.Next() {
214 var o activeOrder
215 if err := rows.Scan(&o.id, &o.symbol, &o.side, &o.typ, &o.status,
216 &o.qty, &o.filled, &o.remaining, &o.price); err != nil {
217 fmt.Println("scan error:", err)
218 return nil
219 }
220 list = append(list, o)
221 }
222 fmt.Println()
223 if len(list) == 0 {
224 fmt.Println(" (no open orders)")
225 return nil
226 }
227 fmt.Printf(" %-3s %-6s %-4s %-6s %-16s %10s %10s %14s\n",
228 "#", "Symbol", "Side", "Type", "Status", "Filled", "Remaining", "Price")
229 fmt.Println(" " + strings.Repeat("-", 84))
230 for i, o := range list {
231 fmt.Printf(" %-3d %-6s %-4s %-6s %-16s %10.4f %10.4f %14.6f\n",
232 i+1, o.symbol, o.side, o.typ, o.status, o.filled, o.remaining, o.price)
233 }
234 return list
235}
236
237// ShowMyOrders - P7 view v_active_orders.
238func ShowMyOrders(s *Session) {
239 listMyOrders(s)
240}
241
242// CancelOrder - P7 stored function project.cancel_order, which releases
243// the reserved cash or crypto of what is still unfilled.
244func CancelOrder(s *Session) {
245 list := listMyOrders(s)
246 if len(list) == 0 {
247 return
248 }
249 n, err := strconv.Atoi(prompt("Order # to cancel (0 = back): "))
250 if err != nil || n < 0 || n > len(list) {
251 fmt.Println("Invalid choice.")
252 return
253 }
254 if n == 0 {
255 return
256 }
257 if _, err := db.DB.Exec(`SELECT cancel_order($1, $2)`, list[n-1].id, s.UserID); err != nil {
258 fmt.Println(dbMessage(err))
259 return
260 }
261 fmt.Println("Order cancelled; its reservation was released.")
262}
Note: See TracBrowser for help on using the repository browser.