diff --git a/init.sql b/init.sql index bfc7df8..796eed8 100644 --- a/init.sql +++ b/init.sql @@ -20,18 +20,18 @@ CREATE TABLE markets( active BOOLEAN DEFAULT true ); CREATE EXTENSION "uuid-ossp"; -CREATE TABLE contracts( +CREATE TABLE shares( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), market_id INTEGER REFERENCES markets(id), description TEXT NOT NULL, - quantity DOUBLE PRECISION NOT NULL + quantity BIGINT NOT NULL DEFAULT 0 ); CREATE TYPE order_side AS ENUM ('BUY', 'SELL'); CREATE TABLE trades( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - contract_id UUID NOT NULL REFERENCES contracts(id), + share_id UUID NOT NULL REFERENCES shares(id), pubkey TEXT NOT NULL REFERENCES users(pubkey), side ORDER_SIDE NOT NULL, - quantity DOUBLE PRECISION NOT NULL, + quantity BIGINT NOT NULL, msats BIGINT NOT NULL ); diff --git a/src/market.go b/src/market.go index 960ed19..076ba7d 100644 --- a/src/market.go +++ b/src/market.go @@ -1,32 +1,20 @@ package main import ( - "errors" "math" ) -// logarithmic market scoring rule (LMSR) market maker from Robin Hanson: -// https://mason.gmu.edu/~rhanson/mktscore.pdf -func BinaryLMSRBuy(invariant int, funding int, tokensAQ float64, tokensBQ float64, sats int) (float64, error) { - k := float64(invariant) - f := float64(funding) - numOutcomes := 2.0 - expA := -tokensAQ / f - expB := -tokensBQ / f - if k != math.Pow(numOutcomes, expA)+math.Pow(numOutcomes, expB) { - // invariant should not already be broken - return -1, errors.New("invariant already broken") - } - // AMM converts order into equal amount of tokens per outcome and then solves equation to fix invariant - // see https://docs.gnosis.io/conditionaltokens/docs/introduction3/#an-example-with-lmsr - newTokensA := tokensAQ + float64(sats) - newTokensB := tokensBQ + float64(sats) - expB = -newTokensB / f - x := newTokensA + f*math.Log(k-math.Pow(numOutcomes, expB))/math.Log(numOutcomes) - expA = -(newTokensA - x) / f - if k != math.Pow(numOutcomes, expA)+math.Pow(numOutcomes, expB) { - // invariant should not be broken - return -1, errors.New("invariant broken") - } - return x, nil +func costFunction(b float64, q1 float64, q2 float64) float64 { + // reference: http://blog.oddhead.com/2006/10/30/implementing-hansons-market-maker/ + return b * math.Log(math.Pow(math.E, q1/b)+math.Pow(math.E, q2/b)) +} + +// logarithmic market scoring rule (LMSR) market maker from Robin Hanson: +// https://mason.gmu.edu/~rhanson/mktscore.pdf1 +func BinaryLMSR(invariant int, funding int, q1 int, q2 int, dq1 int) float64 { + b := float64(funding) + fq1 := float64(q1) + fq2 := float64(q2) + fdq1 := float64(dq1) + return costFunction(b, fq1+fdq1, fq2) - costFunction(b, fq1, fq2) } diff --git a/src/router.go b/src/router.go index 12ae9ec..f9d007b 100644 --- a/src/router.go +++ b/src/router.go @@ -26,18 +26,17 @@ type Market struct { Active bool } -type Contract struct { +type Share struct { Id string MarketId int Description string - Quantity float64 + Quantity int } type MarketDataRequest struct { - ContractId string `json:"contract_id"` - OrderSide string `json:"side"` - Sats int `json:"sats,omitempty"` - Quantity float64 `json:"quantity,omitempty"` + ShareId string `json:"share_id"` + OrderSide string `json:"side"` + Quantity int `json:"quantity"` } func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error { @@ -205,27 +204,27 @@ func market(c echo.Context) error { } else if err != nil { return err } - rows, err := db.Query("SELECT id, market_id, description, quantity FROM contracts WHERE market_id = $1 ORDER BY description DESC", marketId) + rows, err := db.Query("SELECT id, market_id, description, quantity FROM shares WHERE market_id = $1 ORDER BY description DESC", marketId) if err != nil { return err } defer rows.Close() - var contracts []Contract + var shares []Share for rows.Next() { - var contract Contract - rows.Scan(&contract.Id, &contract.MarketId, &contract.Description, &contract.Quantity) - contracts = append(contracts, contract) + var share Share + rows.Scan(&share.Id, &share.MarketId, &share.Description, &share.Quantity) + shares = append(shares, share) } data := map[string]any{ "session": c.Get("session"), "Id": market.Id, "Description": market.Description, - "Contracts": contracts, + "Shares": shares, } return c.Render(http.StatusOK, "binary_market.html", data) } -func marketData(c echo.Context) error { +func marketCost(c echo.Context) error { var req MarketDataRequest err := c.Bind(&req) if err != nil { @@ -241,38 +240,31 @@ func marketData(c echo.Context) error { c.Logger().Error(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 contracts WHERE market_id = $1", marketId) + rows, err := db.Query("SELECT id, market_id, description, quantity FROM shares WHERE market_id = $1", marketId) if err != nil { return err } defer rows.Close() - var contracts []Contract + var shares []Share for rows.Next() { - var contract Contract - rows.Scan(&contract.Id, &contract.MarketId, &contract.Description, &contract.Quantity) - contracts = append(contracts, contract) + var share Share + rows.Scan(&share.Id, &share.MarketId, &share.Description, &share.Quantity) + shares = append(shares, share) } - sats := req.Sats - quantity := req.Quantity - // contract A is always the contract which is bought or sold - contractAIdx := slices.IndexFunc(contracts, func(c Contract) bool { return c.Id == req.ContractId }) - contractBIdx := 0 - if contractAIdx == 0 { - contractBIdx = 1 + 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 } - contractAQ := contracts[contractAIdx].Quantity - contractBQ := contracts[contractBIdx].Quantity - if req.OrderSide == "BUY" && sats > 0 { - quantity, err := BinaryLMSRBuy(1, market.Funding, contractAQ, contractBQ, sats) - if err != nil { - return err - } - return c.JSON(http.StatusOK, map[string]string{"status": "OK", "quantity": fmt.Sprint(quantity)}) + q1 := shares[share1idx].Quantity + q2 := shares[share2idx].Quantity + if req.OrderSide == "SELL" { + dq1 = -dq1 } - if req.OrderSide == "SELL" && quantity > 0 { - // TODO implement BinaryLMSRSell - } - return c.JSON(http.StatusBadRequest, map[string]string{"status": "ERROR", "reason": "bad request"}) + cost := BinaryLMSR(1, market.Funding, q1, q2, dq1) + return c.JSON(http.StatusOK, map[string]string{"status": "OK", "cost": fmt.Sprint(cost)}) } func serve500(c echo.Context) { diff --git a/src/server.go b/src/server.go index 0198d53..5c77a6c 100644 --- a/src/server.go +++ b/src/server.go @@ -61,7 +61,7 @@ func main() { e.GET("/api/session", checkSession) e.POST("/logout", logout) e.GET("/market/:id", sessionGuard(market)) - e.POST("/api/market/:id/data", sessionGuard(marketData)) + e.POST("/api/market/:id/cost", sessionGuard(marketCost)) e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ Format: "${time_custom} ${method} ${uri} ${status}\n", CustomTimeFormat: "2006-01-02 15:04:05.00000-0700", diff --git a/template/binary_market.html b/template/binary_market.html index 4205f0e..b2da54a 100644 --- a/template/binary_market.html +++ b/template/binary_market.html @@ -36,33 +36,37 @@
{{.Description}}
- {{ range .Contracts }} {{ if eq .Description "YES" }} + {{ range .Shares }} {{ if eq .Description "YES" }} {{ else }} {{ end }} {{ end }}
- {{ range .Contracts }} {{ if eq .Description "YES" }} + {{ range .Shares }} {{ if eq .Description "YES" }} {{ else }} {{ end }} @@ -76,7 +80,8 @@ const yesSellBtn = document.querySelector("#yes-sell") const yesSideInput = document.querySelector("#yes-side") const yesQuantityInput = document.querySelector("#yes-quantity") - const yesSatsInput = document.querySelector("#yes-sats") + const yesCostDisplay = document.querySelector("#yes-cost") + const yesCostLabel = document.querySelector("#yes-cost-label") const yesSubmitLabel = document.querySelector("#yes-submit-label") const noOrderBtn = document.querySelector("#no-order") @@ -85,7 +90,8 @@ const noSellBtn = document.querySelector("#no-sell") const noSideInput = document.querySelector("#no-side") const noQuantityInput = document.querySelector("#no-quantity") - const noSatsInput = document.querySelector("#no-sats") + const noCostDisplay = document.querySelector("#no-cost") + const noCostLabel = document.querySelector("#no-cost-label") const noSubmitLabel = document.querySelector("#no-submit-label") yesOrderBtn.onclick = function () { @@ -98,22 +104,28 @@ yesSideInput.value = "BUY" yesBuyBtn.classList.add("selected") yesSellBtn.classList.remove("selected") - yesSubmitLabel.textContent = 'BUY YES contracts' + yesCostLabel.textContent = 'cost [sats]' + yesSubmitLabel.textContent = 'BUY YES shares' + yesQuantityInput.value = undefined + yesCostDisplay.value = undefined } yesSellBtn.onclick = function () { yesSideInput.value = "SELL" yesBuyBtn.classList.remove("selected") yesSellBtn.classList.add("selected") - yesSubmitLabel.textContent = 'SELL NO contracts' + yesCostLabel.textContent = 'payout [sats]' + yesSubmitLabel.textContent = 'SELL NO shares' + yesQuantityInput.value = undefined + yesCostDisplay.value = undefined } - yesSatsInput.onchange = async function(e) { - const sats = parseInt(e.target.value, 10) + yesQuantityInput.onchange = async function(e) { + const quantity = parseInt(e.target.value, 10) const body = { - contract_id: "{{(index .Contracts 0).Id}}", - sats, + share_id: "{{(index .Shares 0).Id}}", + quantity, side: yesSideInput.value } - const rBody = await fetch("/api/market/{{.Id}}/data", { + const rBody = await fetch("/api/market/{{.Id}}/cost", { method: "POST", headers: { "Content-type": "application/json" @@ -126,8 +138,7 @@ return null }) if (!rBody) return null; - const quantity = rBody.quantity - yesQuantityInput.value = quantity + yesCostDisplay.value = parseFloat(Math.abs(rBody.cost)).toFixed(3) } noOrderBtn.onclick = function () { noOrderBtn.classList.add("selected") @@ -139,22 +150,28 @@ noSideInput.value = "BUY" noBuyBtn.classList.add("selected") noSellBtn.classList.remove("selected") - noSubmitLabel.textContent = 'BUY NO contracts' + noCostLabel.textContent = 'cost [sats]' + noSubmitLabel.textContent = 'BUY NO shares' + noQuantityInput.value = undefined + noCostDisplay.value = undefined } noSellBtn.onclick = function () { noSideInput.value = "SELL" noBuyBtn.classList.remove("selected") noSellBtn.classList.add("selected") - noSubmitLabel.textContent = 'SELL YES contracts' + noCostLabel.textContent = 'payout [sats]' + noSubmitLabel.textContent = 'SELL YES shares' + noQuantityInput.value = undefined + noCostDisplay.value = undefined } - noSatsInput.onchange = async function(e) { - const sats = parseInt(e.target.value, 10) + noQuantityInput.onchange = async function(e) { + const quantity = parseInt(e.target.value, 10) const body = { - contract_id: "{{(index .Contracts 1).Id}}", - sats, + share_id: "{{(index .Shares 1).Id}}", + quantity, side: noSideInput.value } - const rBody = await fetch("/api/market/{{.Id}}/data", { + const rBody = await fetch("/api/market/{{.Id}}/cost", { method: "POST", headers: { "Content-type": "application/json" @@ -167,8 +184,7 @@ return null }) if (!rBody) return null; - const quantity = rBody.quantity - noQuantityInput.value = quantity + noCostDisplay.value = parseFloat(Math.abs(rBody.cost)).toFixed(3) }