| 1 | package main
|
|---|
| 2 |
|
|---|
| 3 | import (
|
|---|
| 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.
|
|---|
| 20 | func 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).
|
|---|
| 102 | type optFloat struct {
|
|---|
| 103 | v float64
|
|---|
| 104 | ok bool
|
|---|
| 105 | }
|
|---|
| 106 |
|
|---|
| 107 | func (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.
|
|---|
| 116 | func 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).
|
|---|
| 126 | func 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.
|
|---|
| 160 | func 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.
|
|---|
| 196 | type 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.
|
|---|
| 203 | func 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.
|
|---|
| 238 | func 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.
|
|---|
| 244 | func 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 | }
|
|---|