From 04ce96069b8a01162a3520b196227aef72948b33 Mon Sep 17 00:00:00 2001 From: ekzyis Date: Sat, 9 Sep 2023 22:52:50 +0200 Subject: [PATCH] Add order page --- init.sql | 12 ++- pages/bmarket_order.html | 83 +++++++++++++++ pages/{bmarket.html => bmarket_trade.html} | 10 +- public/index.css | 17 ++++ public/market.css | 8 +- public/order.css | 11 ++ public/order.js | 0 public/{bmarket.js => trade.js} | 11 +- src/funcs.go | 20 ++++ src/lib.go | 8 ++ src/router.go | 113 +++++++++++++++------ src/server.go | 7 +- 12 files changed, 248 insertions(+), 52 deletions(-) create mode 100644 pages/bmarket_order.html rename pages/{bmarket.html => bmarket_trade.html} (92%) create mode 100644 public/order.css create mode 100644 public/order.js rename public/{bmarket.js => trade.js} (91%) create mode 100644 src/funcs.go create mode 100644 src/lib.go diff --git a/init.sql b/init.sql index 796eed8..4fd0715 100644 --- a/init.sql +++ b/init.sql @@ -23,15 +23,19 @@ CREATE EXTENSION "uuid-ossp"; CREATE TABLE shares( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), market_id INTEGER REFERENCES markets(id), - description TEXT NOT NULL, - quantity BIGINT NOT NULL DEFAULT 0 + description TEXT NOT NULL ); CREATE TYPE order_side AS ENUM ('BUY', 'SELL'); -CREATE TABLE trades( +CREATE TABLE orders( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), share_id UUID NOT NULL REFERENCES shares(id), pubkey TEXT NOT NULL REFERENCES users(pubkey), side ORDER_SIDE NOT NULL, quantity BIGINT NOT NULL, - msats BIGINT NOT NULL + price BIGINT NOT NULL +); +CREATE TABLE trades( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + order_id_1 UUID NOT NULL REFERENCES orders(id), + order_id_2 UUID NOT NULL REFERENCES orders(id) ); diff --git a/pages/bmarket_order.html b/pages/bmarket_order.html new file mode 100644 index 0000000..563c6f6 --- /dev/null +++ b/pages/bmarket_order.html @@ -0,0 +1,83 @@ + + + + + delphi.market + + + + + + + + + + {{ if eq .ENV "development" }} + + {{ end }} + + + +
+ +
+
+ + +
+                      _        _   
+ _ __ ___   __ _ _ __| | _____| |_ 
+| '_ ` _ \ / _` | '__| |/ / _ \ __|
+| | | | | | (_| | |  |   <  __/ |_ 
+|_| |_| |_|\__,_|_|  |_|\_\___|\__|
+
+
+
{{.Description}}
+
+ Orders + Trade +
+
+ {{ range .Shares }} + {{.Description}} + {{ end }} +
+ + + + + + + + {{ range .OrderBook }} + + + + + + + {{ end }} +
QuantityPricePriceQuantity
{{.BuyQuantity}} +
+ + {{.BuyPrice}} +
+
+
+ {{.SellPrice}} + +
+
{{.SellQuantity}}
+
+ + + + \ No newline at end of file diff --git a/pages/bmarket.html b/pages/bmarket_trade.html similarity index 92% rename from pages/bmarket.html rename to pages/bmarket_trade.html index f7837b4..e6071da 100644 --- a/pages/bmarket.html +++ b/pages/bmarket_trade.html @@ -40,6 +40,10 @@
{{.Description}}
+
+ Orders + Trade +
{{ range .Shares }} {{ if eq .Description "YES" }} @@ -57,7 +61,7 @@ - + @@ -71,7 +75,7 @@ - + @@ -80,6 +84,6 @@ {{ end }}
- + \ No newline at end of file diff --git a/public/index.css b/public/index.css index 86e01ef..31022ec 100644 --- a/public/index.css +++ b/public/index.css @@ -20,6 +20,10 @@ a:hover { background: #8787A4; color: #ffffff; } +a.selected { + background: #8787A4; + color: #ffffff; +} nav > a { margin: 0 3px; } @@ -130,7 +134,20 @@ ul { .pt-1 { padding-top: 1em; } +.mx-1 { + margin: 0 .2em; +} .word-wrap { word-wrap: break-word; +} + +.align-left { + text-align: left; +} +.align-center { + text-align: center; +} +.align-right { + text-align: right; } \ No newline at end of file diff --git a/public/market.css b/public/market.css index eb4ed1d..7847bfc 100644 --- a/public/market.css +++ b/public/market.css @@ -20,11 +20,11 @@ background-color: rgba(20,158,97,.24); color: #35df8d; } -.yes:hover { +button.yes:hover { background-color: #35df8d; color: white; } -.yes.selected { +button.yes.selected { background-color: #35df8d; color: white; } @@ -33,11 +33,11 @@ background-color: rgba(245,57,94,.24); color: #ff7386; } -.no:hover { +button.no:hover { background-color: #ff7386; color: white; } -.no.selected { +button.no.selected { background-color: #ff7386; color: white; } diff --git a/public/order.css b/public/order.css new file mode 100644 index 0000000..dd089d2 --- /dev/null +++ b/public/order.css @@ -0,0 +1,11 @@ +.align-left { + text-align: left; + } + + .align-center { + text-align: center; + } + + .align-right { + text-align: right; + } \ No newline at end of file diff --git a/public/order.js b/public/order.js new file mode 100644 index 0000000..e69de29 diff --git a/public/bmarket.js b/public/trade.js similarity index 91% rename from public/bmarket.js rename to public/trade.js index a92b4b8..32d54f5 100644 --- a/public/bmarket.js +++ b/public/trade.js @@ -43,6 +43,7 @@ function toggleYesForm() { noForm.style.display = "none" } yesOrderBtn.onclick = toggleYesForm +toggleYesForm() function toggleNoForm() { resetInputs() @@ -62,27 +63,23 @@ function showBuyForm() { resetInputs() yesBuyBtn.classList.add("selected") yesSellBtn.classList.remove("selected") - yesCostLabel.textContent = 'cost [sats]' yesSubmitLabel.textContent = 'BUY YES shares' yesSideInput.value = "BUY" noBuyBtn.classList.add("selected") noSellBtn.classList.remove("selected") - noCostLabel.textContent = 'cost [sats]' - noSubmitLabel.textContent = 'BUY YES shares' + noSubmitLabel.textContent = 'BUY NO shares' noSideInput.value = "BUY" } function showSellForm() { resetInputs() yesBuyBtn.classList.remove("selected") yesSellBtn.classList.add("selected") - yesCostLabel.textContent = 'payout [sats]' yesSubmitLabel.textContent = 'SELL NO shares' yesSideInput.value = "SELL" noBuyBtn.classList.remove("selected") noSellBtn.classList.add("selected") - noCostLabel.textContent = 'payout [sats]' noSubmitLabel.textContent = 'SELL YES shares' noSideInput.value = "SELL" } @@ -130,5 +127,5 @@ function updatePrice(marketId, shareId) { yesCostDisplay.value = parseFloat(Math.abs(rBody.cost)).toFixed(3) } } -yesQuantityInput.oninput = debounce(250)(updatePrice, marketId, yesShareId) -noQuantityInput.onchange = debounce(250)(updatePrice, marketId, noShareId) +// yesQuantityInput.oninput = debounce(250)(updatePrice, marketId, yesShareId) +// noQuantityInput.onchange = debounce(250)(updatePrice, marketId, noShareId) diff --git a/src/funcs.go b/src/funcs.go new file mode 100644 index 0000000..3bf4b8c --- /dev/null +++ b/src/funcs.go @@ -0,0 +1,20 @@ +package main + +import ( + "html/template" +) + +func add(arg1 int, arg2 int) int { + return arg1 + arg2 +} + +func sub(arg1 int, arg2 int) int { + return arg1 - arg2 +} + +var ( + FuncMap template.FuncMap = template.FuncMap{ + "add": add, + "sub": sub, + } +) diff --git a/src/lib.go b/src/lib.go new file mode 100644 index 0000000..36993b5 --- /dev/null +++ b/src/lib.go @@ -0,0 +1,8 @@ +package main + +func Max(x, y int) int { + if x < y { + return y + } + return x +} diff --git a/src/router.go b/src/router.go index 12f7ef6..dae827a 100644 --- a/src/router.go +++ b/src/router.go @@ -12,7 +12,6 @@ import ( "github.com/labstack/echo/v4" "github.com/skip2/go-qrcode" - "golang.org/x/exp/slices" ) type Template struct { @@ -30,7 +29,20 @@ type Share struct { Id string MarketId int Description string - Quantity int +} + +type Order struct { + ShareId string + Side string + Price int + Quantity int +} + +type OrderBookEntry struct { + BuyQuantity int + BuyPrice int + SellPrice int + SellQuantity int } type MarketDataRequest struct { @@ -196,7 +208,7 @@ func logout(c echo.Context) error { return c.Redirect(http.StatusSeeOther, "/") } -func bmarket(c echo.Context) error { +func trades(c echo.Context) error { marketId := c.Param("id") var market Market err := db.QueryRow("SELECT id, description FROM markets WHERE id = $1 AND active = true", marketId).Scan(&market.Id, &market.Description) @@ -205,7 +217,7 @@ func bmarket(c echo.Context) error { } else if err != nil { return err } - rows, err := db.Query("SELECT id, market_id, description, quantity FROM shares WHERE market_id = $1 ORDER BY description DESC", marketId) + rows, err := db.Query("SELECT id, market_id, description FROM shares WHERE market_id = $1 ORDER BY description DESC", marketId) if err != nil { return err } @@ -213,35 +225,30 @@ func bmarket(c echo.Context) error { var shares []Share for rows.Next() { var share Share - rows.Scan(&share.Id, &share.MarketId, &share.Description, &share.Quantity) + rows.Scan(&share.Id, &share.MarketId, &share.Description) shares = append(shares, share) } data := map[string]any{ "session": c.Get("session"), + "ENV": ENV, "Id": market.Id, "Description": market.Description, "Shares": shares, } - return c.Render(http.StatusOK, "bmarket.html", data) + return c.Render(http.StatusOK, "bmarket_trade.html", data) } -func marketCost(c echo.Context) error { - var req MarketDataRequest - err := c.Bind(&req) - if err != nil { - c.Logger().Error(err) - return c.JSON(http.StatusBadRequest, map[string]string{"status": "ERROR", "reason": "bad request"}) - } +func orders(c echo.Context) error { marketId := c.Param("id") + shareId := c.Param("sid") var market Market - err = db.QueryRow("SELECT id, description, funding FROM markets WHERE id = $1 AND active = true", marketId).Scan(&market.Id, &market.Description, &market.Funding) + err := db.QueryRow("SELECT id, description FROM markets WHERE id = $1 AND active = true", marketId).Scan(&market.Id, &market.Description) if err == sql.ErrNoRows { - return c.JSON(http.StatusBadRequest, map[string]string{"status": "ERROR", "reason": "market not found"}) + return echo.NewHTTPError(http.StatusNotFound, "Not Found") } else if err != nil { - c.Logger().Error(err) - return c.JSON(http.StatusInternalServerError, map[string]string{"status": "ERROR", "reason": "internal server error"}) + return err } - rows, err := db.Query("SELECT id, market_id, description, quantity FROM shares WHERE market_id = $1", marketId) + rows, err := db.Query("SELECT id, market_id, description FROM shares WHERE market_id = $1 ORDER BY description DESC", marketId) if err != nil { return err } @@ -249,23 +256,67 @@ func marketCost(c echo.Context) error { var shares []Share for rows.Next() { var share Share - rows.Scan(&share.Id, &share.MarketId, &share.Description, &share.Quantity) + rows.Scan(&share.Id, &share.MarketId, &share.Description) shares = append(shares, share) } - dq1 := req.Quantity - // share 1 is always the share which is bought or sold - share1idx := slices.IndexFunc(shares, func(s Share) bool { return s.Id == req.ShareId }) - share2idx := 0 - if share1idx == 0 { - share2idx = 1 + if shareId == "" { + c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("/market/%s/%s", marketId, shares[0].Id)) } - q1 := shares[share1idx].Quantity - q2 := shares[share2idx].Quantity - if req.OrderSide == "SELL" { - dq1 = -dq1 + rows, err = db.Query(""+ + "SELECT share_id, side, price, SUM(quantity)"+ + "FROM orders WHERE share_id = $1"+ + "GROUP BY (share_id, side, price)"+ + "ORDER BY share_id DESC, side DESC, price DESC", shareId) + if err != nil { + return err } - cost := BinaryLMSR(1, market.Funding, q1, q2, dq1) - return c.JSON(http.StatusOK, map[string]string{"status": "OK", "cost": fmt.Sprint(cost)}) + defer rows.Close() + buyOrders := []Order{} + sellOrders := []Order{} + for rows.Next() { + var order Order + rows.Scan(&order.ShareId, &order.Side, &order.Price, &order.Quantity) + if order.Side == "BUY" { + buyOrders = append(buyOrders, Order{Price: order.Price, Quantity: order.Quantity}) + } else { + sellOrders = append(sellOrders, Order{Price: order.Price, Quantity: order.Quantity}) + } + } + orderBook := []OrderBookEntry{} + buySum := 0 + sellSum := 0 + for i := 0; i < Max(len(buyOrders), len(sellOrders)); i++ { + buyPrice, buyQuantity, sellQuantity, sellPrice := 0, 0, 0, 0 + if i < len(buyOrders) { + buyPrice = buyOrders[i].Price + buyQuantity = buySum + buyOrders[i].Quantity + } + if i < len(sellOrders) { + sellPrice = sellOrders[i].Price + sellQuantity = sellSum + sellOrders[i].Quantity + } + buySum += buyQuantity + sellSum += sellQuantity + orderBook = append( + orderBook, + OrderBookEntry{ + BuyQuantity: buyQuantity, + BuyPrice: buyPrice, + SellPrice: sellPrice, + SellQuantity: sellQuantity, + }, + ) + } + data := map[string]any{ + "session": c.Get("session"), + "ENV": ENV, + "Id": market.Id, + "Description": market.Description, + "ShareId": shareId, + "Shares": shares, + "OrderBook": orderBook, + } + return c.Render(http.StatusOK, "bmarket_order.html", data) } func serve500(c echo.Context) { diff --git a/src/server.go b/src/server.go index 6cc73e0..3ca0057 100644 --- a/src/server.go +++ b/src/server.go @@ -45,7 +45,7 @@ func init() { flag.Parse() e = echo.New() t = &Template{ - templates: template.Must(template.ParseGlob("pages/**.html")), + templates: template.Must(template.New("").Funcs(FuncMap).ParseGlob("pages/**.html")), } COMMIT_LONG_SHA = execCmd("git", "rev-parse", "HEAD") COMMIT_SHORT_SHA = execCmd("git", "rev-parse", "--short", "HEAD") @@ -63,8 +63,9 @@ func main() { e.GET("/api/login", verifyLogin) e.GET("/api/session", checkSession) e.POST("/logout", logout) - e.GET("/market/:id", sessionGuard(bmarket)) - e.POST("/api/market/:id/cost", sessionGuard(marketCost)) + e.GET("/market/:id", sessionGuard(orders)) + e.GET("/market/:id/:sid", sessionGuard(orders)) + e.GET("/market/:id/trade", sessionGuard(trades)) e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ Format: "${time_custom} ${method} ${uri} ${status}\n", CustomTimeFormat: "2006-01-02 15:04:05.00000-0700",