Add order page

This commit is contained in:
ekzyis 2023-09-09 22:52:50 +02:00
parent 58901e8d7e
commit 04ce96069b
12 changed files with 248 additions and 52 deletions

View File

@ -23,15 +23,19 @@ CREATE EXTENSION "uuid-ossp";
CREATE TABLE shares( CREATE TABLE shares(
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
market_id INTEGER REFERENCES markets(id), market_id INTEGER REFERENCES markets(id),
description TEXT NOT NULL, description TEXT NOT NULL
quantity BIGINT NOT NULL DEFAULT 0
); );
CREATE TYPE order_side AS ENUM ('BUY', 'SELL'); CREATE TYPE order_side AS ENUM ('BUY', 'SELL');
CREATE TABLE trades( CREATE TABLE orders(
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
share_id UUID NOT NULL REFERENCES shares(id), share_id UUID NOT NULL REFERENCES shares(id),
pubkey TEXT NOT NULL REFERENCES users(pubkey), pubkey TEXT NOT NULL REFERENCES users(pubkey),
side ORDER_SIDE NOT NULL, side ORDER_SIDE NOT NULL,
quantity BIGINT 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)
); );

83
pages/bmarket_order.html Normal file
View File

@ -0,0 +1,83 @@
<!DOCTYPE html>
<html>
<head>
<title>delphi.market</title>
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
<link rel="manifest" href="/site.webmanifest" />
<link rel="stylesheet" href="/index.css" />
<link rel="stylesheet" href="/market.css" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#091833" />
{{ if eq .ENV "development" }}
<script defer src="/hotreload.js"></script>
{{ end }}
</head>
<body>
<header class="flex flex-row text-center justify-center pt-1">
<nav>
<a href="/">home</a>
{{ if .session }}
<form action='/logout' method='post'>
<button type='submit'>logout</button>
</form>
{{ else }} <a href="/login">login</a> {{ end }}
</nav>
</header>
<div class="container flex flex-column text-center">
<code>
<strong>
<pre>
_ _
_ __ ___ __ _ _ __| | _____| |_
| '_ ` _ \ / _` | '__| |/ / _ \ __|
| | | | | | (_| | | | < __/ |_
|_| |_| |_|\__,_|_| |_|\_\___|\__|</pre>
</strong>
</code>
<div class="font-mono mb-1">{{.Description}}</div>
<div class="flex justify-start mb-1">
<a class="mx-1 selected" href="/market/{{.Id}}">Orders</a>
<a class="mx-1" href="/market/{{.Id}}/trade">Trade</a>
</div>
<div class="flex flex-row justify-center mb-1">
{{ range .Shares }}
<a class="mx-1 {{ if eq $.ShareId .Id }}selected{{end}}"
href="/market/{{$.Id}}/{{.Id}}">{{.Description}}</a>
{{ end }}
</div>
<table>
<tr>
<th class="align-left">Quantity</th>
<th class="align-right">Price</th>
<th class="align-left">Price</th>
<th class="align-right">Quantity</th>
</tr>
{{ range .OrderBook }}
<tr>
<td style="width: auto" class="align-left">{{.BuyQuantity}}</td>
<td style="width: 50%">
<div class="flex">
<span style="width: {{ sub 100 .BuyQuantity}}%"></span>
<span style="width: {{.BuyQuantity}}%" class="align-right yes">{{.BuyPrice}}</span>
</div>
</td>
<td style="width: 50%">
<div class="flex">
<span style="width: {{.SellQuantity}}%" class="align-left no">{{.SellPrice}}</span>
<span style="width: {{ sub 100 .SellQuantity}}%"></span>
</div>
</td>
<td style="width: auto" class="align-right">{{.SellQuantity}}</td>
</tr>
{{ end }}
</table>
</div>
</body>
<script src="/order.js"></script>
</html>

View File

@ -40,6 +40,10 @@
</strong> </strong>
</code> </code>
<div class="font-mono mb-1">{{.Description}}</div> <div class="font-mono mb-1">{{.Description}}</div>
<div class="flex justify-start mb-1">
<a class="mx-1" href="/market/{{.Id}}">Orders</a>
<a class="mx-1 selected" href="/market/{{.Id}}/trade">Trade</a>
</div>
<div class="flex flex-row justify-center mb-1"> <div class="flex flex-row justify-center mb-1">
{{ range .Shares }} {{ if eq .Description "YES" }} {{ range .Shares }} {{ if eq .Description "YES" }}
<button id="yes-order" class="order-button sx-1 yes">YES</button> <button id="yes-order" class="order-button sx-1 yes">YES</button>
@ -57,7 +61,7 @@
<input id="yes-side" hidden name="side" value="BUY" /> <input id="yes-side" hidden name="side" value="BUY" />
<label>shares</label> <label>shares</label>
<input id="yes-quantity" type="number" name="quantity" placeholder="quantity" /> <input id="yes-quantity" type="number" name="quantity" placeholder="quantity" />
<label id="yes-cost-label">cost [sats]</label> <label>price [sats]</label>
<input id="yes-cost" type="number" name="cost" disabled /> <input id="yes-cost" type="number" name="cost" disabled />
<label id="yes-submit-label">BUY YES shares</label> <label id="yes-submit-label">BUY YES shares</label>
<button type="submit">SUBMIT</button> <button type="submit">SUBMIT</button>
@ -71,7 +75,7 @@
<input id="no-side" hidden name="side" value="BUY" /> <input id="no-side" hidden name="side" value="BUY" />
<label>shares</label> <label>shares</label>
<input id="no-quantity" type="number" name="quantity" placeholder="quantity" /> <input id="no-quantity" type="number" name="quantity" placeholder="quantity" />
<label id="no-cost-label">cost [sats]</label> <label>price [sats]</label>
<input id="no-cost" type="number" name="cost" disabled /> <input id="no-cost" type="number" name="cost" disabled />
<label id="no-submit-label">BUY NO shares</label> <label id="no-submit-label">BUY NO shares</label>
<button type="submit">SUBMIT</button> <button type="submit">SUBMIT</button>
@ -80,6 +84,6 @@
{{ end }} {{ end }}
</div> </div>
</body> </body>
<script src="/bmarket.js"></script> <script src="/trade.js"></script>
</html> </html>

View File

@ -20,6 +20,10 @@ a:hover {
background: #8787A4; background: #8787A4;
color: #ffffff; color: #ffffff;
} }
a.selected {
background: #8787A4;
color: #ffffff;
}
nav > a { nav > a {
margin: 0 3px; margin: 0 3px;
} }
@ -130,7 +134,20 @@ ul {
.pt-1 { .pt-1 {
padding-top: 1em; padding-top: 1em;
} }
.mx-1 {
margin: 0 .2em;
}
.word-wrap { .word-wrap {
word-wrap: break-word; word-wrap: break-word;
} }
.align-left {
text-align: left;
}
.align-center {
text-align: center;
}
.align-right {
text-align: right;
}

View File

@ -20,11 +20,11 @@
background-color: rgba(20,158,97,.24); background-color: rgba(20,158,97,.24);
color: #35df8d; color: #35df8d;
} }
.yes:hover { button.yes:hover {
background-color: #35df8d; background-color: #35df8d;
color: white; color: white;
} }
.yes.selected { button.yes.selected {
background-color: #35df8d; background-color: #35df8d;
color: white; color: white;
} }
@ -33,11 +33,11 @@
background-color: rgba(245,57,94,.24); background-color: rgba(245,57,94,.24);
color: #ff7386; color: #ff7386;
} }
.no:hover { button.no:hover {
background-color: #ff7386; background-color: #ff7386;
color: white; color: white;
} }
.no.selected { button.no.selected {
background-color: #ff7386; background-color: #ff7386;
color: white; color: white;
} }

11
public/order.css Normal file
View File

@ -0,0 +1,11 @@
.align-left {
text-align: left;
}
.align-center {
text-align: center;
}
.align-right {
text-align: right;
}

0
public/order.js Normal file
View File

View File

@ -43,6 +43,7 @@ function toggleYesForm() {
noForm.style.display = "none" noForm.style.display = "none"
} }
yesOrderBtn.onclick = toggleYesForm yesOrderBtn.onclick = toggleYesForm
toggleYesForm()
function toggleNoForm() { function toggleNoForm() {
resetInputs() resetInputs()
@ -62,27 +63,23 @@ function showBuyForm() {
resetInputs() resetInputs()
yesBuyBtn.classList.add("selected") yesBuyBtn.classList.add("selected")
yesSellBtn.classList.remove("selected") yesSellBtn.classList.remove("selected")
yesCostLabel.textContent = 'cost [sats]'
yesSubmitLabel.textContent = 'BUY YES shares' yesSubmitLabel.textContent = 'BUY YES shares'
yesSideInput.value = "BUY" yesSideInput.value = "BUY"
noBuyBtn.classList.add("selected") noBuyBtn.classList.add("selected")
noSellBtn.classList.remove("selected") noSellBtn.classList.remove("selected")
noCostLabel.textContent = 'cost [sats]' noSubmitLabel.textContent = 'BUY NO shares'
noSubmitLabel.textContent = 'BUY YES shares'
noSideInput.value = "BUY" noSideInput.value = "BUY"
} }
function showSellForm() { function showSellForm() {
resetInputs() resetInputs()
yesBuyBtn.classList.remove("selected") yesBuyBtn.classList.remove("selected")
yesSellBtn.classList.add("selected") yesSellBtn.classList.add("selected")
yesCostLabel.textContent = 'payout [sats]'
yesSubmitLabel.textContent = 'SELL NO shares' yesSubmitLabel.textContent = 'SELL NO shares'
yesSideInput.value = "SELL" yesSideInput.value = "SELL"
noBuyBtn.classList.remove("selected") noBuyBtn.classList.remove("selected")
noSellBtn.classList.add("selected") noSellBtn.classList.add("selected")
noCostLabel.textContent = 'payout [sats]'
noSubmitLabel.textContent = 'SELL YES shares' noSubmitLabel.textContent = 'SELL YES shares'
noSideInput.value = "SELL" noSideInput.value = "SELL"
} }
@ -130,5 +127,5 @@ function updatePrice(marketId, shareId) {
yesCostDisplay.value = parseFloat(Math.abs(rBody.cost)).toFixed(3) yesCostDisplay.value = parseFloat(Math.abs(rBody.cost)).toFixed(3)
} }
} }
yesQuantityInput.oninput = debounce(250)(updatePrice, marketId, yesShareId) // yesQuantityInput.oninput = debounce(250)(updatePrice, marketId, yesShareId)
noQuantityInput.onchange = debounce(250)(updatePrice, marketId, noShareId) // noQuantityInput.onchange = debounce(250)(updatePrice, marketId, noShareId)

20
src/funcs.go Normal file
View File

@ -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,
}
)

8
src/lib.go Normal file
View File

@ -0,0 +1,8 @@
package main
func Max(x, y int) int {
if x < y {
return y
}
return x
}

View File

@ -12,7 +12,6 @@ import (
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"github.com/skip2/go-qrcode" "github.com/skip2/go-qrcode"
"golang.org/x/exp/slices"
) )
type Template struct { type Template struct {
@ -30,7 +29,20 @@ type Share struct {
Id string Id string
MarketId int MarketId int
Description string 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 { type MarketDataRequest struct {
@ -196,7 +208,7 @@ func logout(c echo.Context) error {
return c.Redirect(http.StatusSeeOther, "/") return c.Redirect(http.StatusSeeOther, "/")
} }
func bmarket(c echo.Context) error { func trades(c echo.Context) error {
marketId := c.Param("id") marketId := c.Param("id")
var market Market var market Market
err := db.QueryRow("SELECT id, description FROM markets WHERE id = $1 AND active = true", marketId).Scan(&market.Id, &market.Description) 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 { } else if err != nil {
return err 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 { if err != nil {
return err return err
} }
@ -213,35 +225,30 @@ func bmarket(c echo.Context) error {
var shares []Share var shares []Share
for rows.Next() { for rows.Next() {
var share Share 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) shares = append(shares, share)
} }
data := map[string]any{ data := map[string]any{
"session": c.Get("session"), "session": c.Get("session"),
"ENV": ENV,
"Id": market.Id, "Id": market.Id,
"Description": market.Description, "Description": market.Description,
"Shares": shares, "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 { func orders(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"})
}
marketId := c.Param("id") marketId := c.Param("id")
shareId := c.Param("sid")
var market Market 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 { 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 { } else if err != nil {
c.Logger().Error(err) return err
return c.JSON(http.StatusInternalServerError, map[string]string{"status": "ERROR", "reason": "internal server error"})
} }
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 { if err != nil {
return err return err
} }
@ -249,23 +256,67 @@ func marketCost(c echo.Context) error {
var shares []Share var shares []Share
for rows.Next() { for rows.Next() {
var share Share 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) shares = append(shares, share)
} }
dq1 := req.Quantity if shareId == "" {
// share 1 is always the share which is bought or sold c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("/market/%s/%s", marketId, shares[0].Id))
share1idx := slices.IndexFunc(shares, func(s Share) bool { return s.Id == req.ShareId })
share2idx := 0
if share1idx == 0 {
share2idx = 1
} }
q1 := shares[share1idx].Quantity rows, err = db.Query(""+
q2 := shares[share2idx].Quantity "SELECT share_id, side, price, SUM(quantity)"+
if req.OrderSide == "SELL" { "FROM orders WHERE share_id = $1"+
dq1 = -dq1 "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) defer rows.Close()
return c.JSON(http.StatusOK, map[string]string{"status": "OK", "cost": fmt.Sprint(cost)}) 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) { func serve500(c echo.Context) {

View File

@ -45,7 +45,7 @@ func init() {
flag.Parse() flag.Parse()
e = echo.New() e = echo.New()
t = &Template{ 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_LONG_SHA = execCmd("git", "rev-parse", "HEAD")
COMMIT_SHORT_SHA = execCmd("git", "rev-parse", "--short", "HEAD") COMMIT_SHORT_SHA = execCmd("git", "rev-parse", "--short", "HEAD")
@ -63,8 +63,9 @@ func main() {
e.GET("/api/login", verifyLogin) e.GET("/api/login", verifyLogin)
e.GET("/api/session", checkSession) e.GET("/api/session", checkSession)
e.POST("/logout", logout) e.POST("/logout", logout)
e.GET("/market/:id", sessionGuard(bmarket)) e.GET("/market/:id", sessionGuard(orders))
e.POST("/api/market/:id/cost", sessionGuard(marketCost)) e.GET("/market/:id/:sid", sessionGuard(orders))
e.GET("/market/:id/trade", sessionGuard(trades))
e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
Format: "${time_custom} ${method} ${uri} ${status}\n", Format: "${time_custom} ${method} ${uri} ${status}\n",
CustomTimeFormat: "2006-01-02 15:04:05.00000-0700", CustomTimeFormat: "2006-01-02 15:04:05.00000-0700",