Use cost function

With this cost function, buying and selling is a lot easier since it uses the same function.

Also, shares are now always integers which is also easier to grasp.

Reference: http://blog.oddhead.com/2006/10/30/implementing-hansons-market-maker/
This commit is contained in:
ekzyis 2023-09-09 22:52:50 +02:00
parent 87ce57c862
commit 7094438152
5 changed files with 93 additions and 97 deletions

View File

@ -20,18 +20,18 @@ CREATE TABLE markets(
active BOOLEAN DEFAULT true active BOOLEAN DEFAULT true
); );
CREATE EXTENSION "uuid-ossp"; CREATE EXTENSION "uuid-ossp";
CREATE TABLE contracts( 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 DOUBLE PRECISION 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 trades(
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), 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), pubkey TEXT NOT NULL REFERENCES users(pubkey),
side ORDER_SIDE NOT NULL, side ORDER_SIDE NOT NULL,
quantity DOUBLE PRECISION NOT NULL, quantity BIGINT NOT NULL,
msats BIGINT NOT NULL msats BIGINT NOT NULL
); );

View File

@ -1,32 +1,20 @@
package main package main
import ( import (
"errors"
"math" "math"
) )
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: // logarithmic market scoring rule (LMSR) market maker from Robin Hanson:
// https://mason.gmu.edu/~rhanson/mktscore.pdf // https://mason.gmu.edu/~rhanson/mktscore.pdf1
func BinaryLMSRBuy(invariant int, funding int, tokensAQ float64, tokensBQ float64, sats int) (float64, error) { func BinaryLMSR(invariant int, funding int, q1 int, q2 int, dq1 int) float64 {
k := float64(invariant) b := float64(funding)
f := float64(funding) fq1 := float64(q1)
numOutcomes := 2.0 fq2 := float64(q2)
expA := -tokensAQ / f fdq1 := float64(dq1)
expB := -tokensBQ / f return costFunction(b, fq1+fdq1, fq2) - costFunction(b, fq1, fq2)
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
} }

View File

@ -26,18 +26,17 @@ type Market struct {
Active bool Active bool
} }
type Contract struct { type Share struct {
Id string Id string
MarketId int MarketId int
Description string Description string
Quantity float64 Quantity int
} }
type MarketDataRequest struct { type MarketDataRequest struct {
ContractId string `json:"contract_id"` ShareId string `json:"share_id"`
OrderSide string `json:"side"` OrderSide string `json:"side"`
Sats int `json:"sats,omitempty"` Quantity int `json:"quantity"`
Quantity float64 `json:"quantity,omitempty"`
} }
func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error { 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 { } else if err != nil {
return err 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 { if err != nil {
return err return err
} }
defer rows.Close() defer rows.Close()
var contracts []Contract var shares []Share
for rows.Next() { for rows.Next() {
var contract Contract var share Share
rows.Scan(&contract.Id, &contract.MarketId, &contract.Description, &contract.Quantity) rows.Scan(&share.Id, &share.MarketId, &share.Description, &share.Quantity)
contracts = append(contracts, contract) shares = append(shares, share)
} }
data := map[string]any{ data := map[string]any{
"session": c.Get("session"), "session": c.Get("session"),
"Id": market.Id, "Id": market.Id,
"Description": market.Description, "Description": market.Description,
"Contracts": contracts, "Shares": shares,
} }
return c.Render(http.StatusOK, "binary_market.html", data) return c.Render(http.StatusOK, "binary_market.html", data)
} }
func marketData(c echo.Context) error { func marketCost(c echo.Context) error {
var req MarketDataRequest var req MarketDataRequest
err := c.Bind(&req) err := c.Bind(&req)
if err != nil { if err != nil {
@ -241,38 +240,31 @@ func marketData(c echo.Context) error {
c.Logger().Error(err) c.Logger().Error(err)
return c.JSON(http.StatusInternalServerError, map[string]string{"status": "ERROR", "reason": "internal server error"}) 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 { if err != nil {
return err return err
} }
defer rows.Close() defer rows.Close()
var contracts []Contract var shares []Share
for rows.Next() { for rows.Next() {
var contract Contract var share Share
rows.Scan(&contract.Id, &contract.MarketId, &contract.Description, &contract.Quantity) rows.Scan(&share.Id, &share.MarketId, &share.Description, &share.Quantity)
contracts = append(contracts, contract) shares = append(shares, share)
} }
sats := req.Sats dq1 := req.Quantity
quantity := req.Quantity // share 1 is always the share which is bought or sold
// contract A is always the contract which is bought or sold share1idx := slices.IndexFunc(shares, func(s Share) bool { return s.Id == req.ShareId })
contractAIdx := slices.IndexFunc(contracts, func(c Contract) bool { return c.Id == req.ContractId }) share2idx := 0
contractBIdx := 0 if share1idx == 0 {
if contractAIdx == 0 { share2idx = 1
contractBIdx = 1
} }
contractAQ := contracts[contractAIdx].Quantity q1 := shares[share1idx].Quantity
contractBQ := contracts[contractBIdx].Quantity q2 := shares[share2idx].Quantity
if req.OrderSide == "BUY" && sats > 0 { if req.OrderSide == "SELL" {
quantity, err := BinaryLMSRBuy(1, market.Funding, contractAQ, contractBQ, sats) dq1 = -dq1
if err != nil {
return err
} }
return c.JSON(http.StatusOK, map[string]string{"status": "OK", "quantity": fmt.Sprint(quantity)}) cost := BinaryLMSR(1, market.Funding, q1, q2, dq1)
} return c.JSON(http.StatusOK, map[string]string{"status": "OK", "cost": fmt.Sprint(cost)})
if req.OrderSide == "SELL" && quantity > 0 {
// TODO implement BinaryLMSRSell
}
return c.JSON(http.StatusBadRequest, map[string]string{"status": "ERROR", "reason": "bad request"})
} }
func serve500(c echo.Context) { func serve500(c echo.Context) {

View File

@ -61,7 +61,7 @@ func main() {
e.GET("/api/session", checkSession) e.GET("/api/session", checkSession)
e.POST("/logout", logout) e.POST("/logout", logout)
e.GET("/market/:id", sessionGuard(market)) 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{ 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",

View File

@ -36,33 +36,37 @@
</code> </code>
<div class="font-mono mb-1">{{.Description}}</div> <div class="font-mono mb-1">{{.Description}}</div>
<div class="flex flex-row justify-center mb-1"> <div class="flex flex-row justify-center mb-1">
{{ range .Contracts }} {{ 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>
{{ else }} {{ else }}
<button id="no-order" class="order-button sx-1 no">NO</button> <button id="no-order" class="order-button sx-1 no">NO</button>
{{ end }} {{ end }}
{{ end }} {{ end }}
</div> </div>
{{ range .Contracts }} {{ if eq .Description "YES" }} {{ range .Shares }} {{ if eq .Description "YES" }}
<form id="yes-form" class="order-form" hidden action="/api/market/{{$.Id}}/order" method="post"> <form id="yes-form" class="order-form" hidden action="/api/market/{{$.Id}}/order" method="post">
<button id="yes-buy" type="button" class="order-button yes w-100p selected">BUY</button> <button id="yes-buy" type="button" class="order-button yes w-100p selected">BUY</button>
<button id="yes-sell" type="button" class="order-button no w-100p">SELL</button> <button id="yes-sell" type="button" class="order-button no w-100p">SELL</button>
<input hidden name="contract_id" value="{{.Id}}" /> <input hidden name="share_id" value="{{.Id}}" />
<input id="yes-side" hidden name="side" value="BUY" /> <input id="yes-side" hidden name="side" value="BUY" />
<input id="yes-quantity" type="number" name="quantity" placeholder="quantity" disabled /> <label>shares</label>
<input id="yes-sats" type="number" name="sats" placeholder="sats"/> <input id="yes-quantity" type="number" name="quantity" placeholder="quantity" />
<label id="yes-submit-label">BUY YES contracts</label> <label id="yes-cost-label">cost [sats]</label>
<input id="yes-cost" type="number" name="cost" disabled />
<label id="yes-submit-label">BUY YES shares</label>
<button type="submit">SUBMIT</button> <button type="submit">SUBMIT</button>
</form> </form>
{{ else }} {{ else }}
<form id="no-form" class="order-form" hidden action="/api/market/{{$.Id}}/order" method="post"> <form id="no-form" class="order-form" hidden action="/api/market/{{$.Id}}/order" method="post">
<button id="no-buy" type="button" class="order-button yes w-100p selected">BUY</button> <button id="no-buy" type="button" class="order-button yes w-100p selected">BUY</button>
<button id="no-sell" type="button" class="order-button no w-100p">SELL</button> <button id="no-sell" type="button" class="order-button no w-100p">SELL</button>
<input hidden name="contract_id" value="{{.Id}}" /> <input hidden name="share_id" value="{{.Id}}" />
<input id="no-side" hidden name="side" value="BUY" /> <input id="no-side" hidden name="side" value="BUY" />
<input id="no-quantity" type="number" name="quantity" placeholder="quantity" disabled /> <label>shares</label>
<input id="no-sats" type="number" name="sats" placeholder="sats" /> <input id="no-quantity" type="number" name="quantity" placeholder="quantity" />
<label id="no-submit-label">BUY NO contracts</label> <label id="no-cost-label">cost [sats]</label>
<input id="no-cost" type="number" name="cost" disabled />
<label id="no-submit-label">BUY NO shares</label>
<button type="submit">SUBMIT</button> <button type="submit">SUBMIT</button>
</form> </form>
{{ end }} {{ end }}
@ -76,7 +80,8 @@
const yesSellBtn = document.querySelector("#yes-sell") const yesSellBtn = document.querySelector("#yes-sell")
const yesSideInput = document.querySelector("#yes-side") const yesSideInput = document.querySelector("#yes-side")
const yesQuantityInput = document.querySelector("#yes-quantity") 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 yesSubmitLabel = document.querySelector("#yes-submit-label")
const noOrderBtn = document.querySelector("#no-order") const noOrderBtn = document.querySelector("#no-order")
@ -85,7 +90,8 @@
const noSellBtn = document.querySelector("#no-sell") const noSellBtn = document.querySelector("#no-sell")
const noSideInput = document.querySelector("#no-side") const noSideInput = document.querySelector("#no-side")
const noQuantityInput = document.querySelector("#no-quantity") 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") const noSubmitLabel = document.querySelector("#no-submit-label")
yesOrderBtn.onclick = function () { yesOrderBtn.onclick = function () {
@ -98,22 +104,28 @@
yesSideInput.value = "BUY" yesSideInput.value = "BUY"
yesBuyBtn.classList.add("selected") yesBuyBtn.classList.add("selected")
yesSellBtn.classList.remove("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 () { yesSellBtn.onclick = function () {
yesSideInput.value = "SELL" yesSideInput.value = "SELL"
yesBuyBtn.classList.remove("selected") yesBuyBtn.classList.remove("selected")
yesSellBtn.classList.add("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) { yesQuantityInput.onchange = async function(e) {
const sats = parseInt(e.target.value, 10) const quantity = parseInt(e.target.value, 10)
const body = { const body = {
contract_id: "{{(index .Contracts 0).Id}}", share_id: "{{(index .Shares 0).Id}}",
sats, quantity,
side: yesSideInput.value side: yesSideInput.value
} }
const rBody = await fetch("/api/market/{{.Id}}/data", { const rBody = await fetch("/api/market/{{.Id}}/cost", {
method: "POST", method: "POST",
headers: { headers: {
"Content-type": "application/json" "Content-type": "application/json"
@ -126,8 +138,7 @@
return null return null
}) })
if (!rBody) return null; if (!rBody) return null;
const quantity = rBody.quantity yesCostDisplay.value = parseFloat(Math.abs(rBody.cost)).toFixed(3)
yesQuantityInput.value = quantity
} }
noOrderBtn.onclick = function () { noOrderBtn.onclick = function () {
noOrderBtn.classList.add("selected") noOrderBtn.classList.add("selected")
@ -139,22 +150,28 @@
noSideInput.value = "BUY" noSideInput.value = "BUY"
noBuyBtn.classList.add("selected") noBuyBtn.classList.add("selected")
noSellBtn.classList.remove("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 () { noSellBtn.onclick = function () {
noSideInput.value = "SELL" noSideInput.value = "SELL"
noBuyBtn.classList.remove("selected") noBuyBtn.classList.remove("selected")
noSellBtn.classList.add("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) { noQuantityInput.onchange = async function(e) {
const sats = parseInt(e.target.value, 10) const quantity = parseInt(e.target.value, 10)
const body = { const body = {
contract_id: "{{(index .Contracts 1).Id}}", share_id: "{{(index .Shares 1).Id}}",
sats, quantity,
side: noSideInput.value side: noSideInput.value
} }
const rBody = await fetch("/api/market/{{.Id}}/data", { const rBody = await fetch("/api/market/{{.Id}}/cost", {
method: "POST", method: "POST",
headers: { headers: {
"Content-type": "application/json" "Content-type": "application/json"
@ -167,8 +184,7 @@
return null return null
}) })
if (!rBody) return null; if (!rBody) return null;
const quantity = rBody.quantity noCostDisplay.value = parseFloat(Math.abs(rBody.cost)).toFixed(3)
noQuantityInput.value = quantity
} }
</script> </script>
</html> </html>