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:
parent
87ce57c862
commit
7094438152
8
init.sql
8
init.sql
|
@ -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
|
||||||
);
|
);
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in New Issue