Calculate probabilities using LMSR

This commit is contained in:
ekzyis 2024-08-25 00:48:00 -05:00
parent d87b97d235
commit bd7b2715bd
4 changed files with 105 additions and 27 deletions

View File

@ -42,34 +42,19 @@ CREATE TABLE markets(
end_date TIMESTAMP WITH TIME ZONE NOT NULL,
settled_at TIMESTAMP WITH TIME ZONE,
user_id INTEGER NOT NULL REFERENCES users(id),
invoice_id INTEGER NOT NULL UNIQUE REFERENCES invoices(id)
invoice_id INTEGER NOT NULL UNIQUE REFERENCES invoices(id),
lmsr_b FLOAT NOT NULL
);
CREATE TABLE shares(
id SERIAL PRIMARY KEY,
market_id INTEGER NOT NULL REFERENCES markets(id),
description TEXT NOT NULL,
win BOOLEAN
);
CREATE TYPE order_side AS ENUM ('BUY', 'SELL');
CREATE TABLE orders(
id SERIAL PRIMARY KEY,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP WITH TIME ZONE,
share_id INTEGER NOT NULL REFERENCES shares(id),
market_id INTEGER NOT NULL REFERENCES markets(id),
user_id INTEGER NOT NULL REFERENCES users(id),
side ORDER_SIDE NOT NULL,
quantity BIGINT NOT NULL,
price BIGINT NOT NULL,
invoice_id INTEGER REFERENCES invoices(id),
order_id INTEGER REFERENCES orders(id)
outcome INTEGER NOT NULL,
invoice_id INTEGER REFERENCES invoices(id)
);
ALTER TABLE orders ADD CONSTRAINT order_price CHECK(price > 0 AND price < 100);
ALTER TABLE orders ADD CONSTRAINT order_quantity CHECK(quantity > 0);
CREATE TABLE withdrawals(
id SERIAL PRIMARY KEY,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,

View File

@ -3,9 +3,11 @@ package handler
import (
"database/sql"
"fmt"
"math"
"net/http"
"time"
"git.ekzyis.com/ekzyis/delphi.market/lib/lmsr"
"git.ekzyis.com/ekzyis/delphi.market/server/router/context"
"git.ekzyis.com/ekzyis/delphi.market/server/router/pages"
"git.ekzyis.com/ekzyis/delphi.market/server/router/pages/components"
@ -30,7 +32,14 @@ func HandleCreate(sc context.Context) echo.HandlerFunc {
endDate = c.FormValue("end_date")
hash lntypes.Hash
paymentRequest string
cost = lnwire.MilliSatoshi(1000e3)
cost = lnwire.MilliSatoshi(10_000e3) // creating a market costs 10k sats
// The cost is used to fund the market.
// Maximum possible amount of money the market maker can lose is b*ln2.
// This means if we can only payout as many sats as we paid for the market,
// we need to solve for b: b = cost / ln2
b = float64(cost) / math.Log(2)
expiry = int64(600)
expiresAt = time.Now().Add(time.Second * time.Duration(expiry))
invoiceId int
@ -62,10 +71,10 @@ func HandleCreate(sc context.Context) echo.HandlerFunc {
}
if err = tx.QueryRowContext(ctx, ""+
"INSERT INTO markets (question, description, end_date, user_id, invoice_id) "+
"VALUES ($1, $2, $3, $4, $5) "+
"INSERT INTO markets (question, description, end_date, user_id, invoice_id, lmsr_b) "+
"VALUES ($1, $2, $3, $4, $5, $6) "+
"RETURNING id",
question, description, endDate, u.Id, invoiceId).Scan(&marketId); err != nil {
question, description, endDate, u.Id, invoiceId, b).Scan(&marketId); err != nil {
return err
}
@ -94,16 +103,77 @@ func HandleMarket(sc context.Context) echo.HandlerFunc {
id = c.Param("id")
m = types.Market{}
u = types.User{}
l = types.LSMR{}
err error
)
if err = db.QueryRowContext(ctx, ""+
"SELECT m.id, m.question, m.description, m.created_at, m.end_date, "+
"SELECT m.id, m.question, m.description, m.created_at, m.end_date, m.lmsr_b, "+
"u.id, u.name, u.created_at, u.ln_pubkey, u.nostr_pubkey, u.msats "+
"FROM markets m "+
"JOIN users u ON m.user_id = u.id "+
"JOIN invoices i ON m.invoice_id = i.id "+
"WHERE m.id = $1 AND i.confirmed_at IS NOT NULL", id).Scan(
&m.Id, &m.Question, &m.Description, &m.CreatedAt, &m.EndDate, &l.B,
&u.Id, &u.Name, &u.CreatedAt, &u.LnPubkey, &u.NostrPubkey, &u.Msats); err != nil {
if err == sql.ErrNoRows {
return echo.NewHTTPError(http.StatusNotFound)
}
return err
}
m.User = u
if err = db.QueryRowContext(ctx, ""+
"SELECT "+
"COUNT(o.quantity) FILTER(WHERE o.outcome = 0) AS q1, "+
"COUNT(o.quantity) FILTER(WHERE o.outcome = 1) AS q2 "+
"FROM orders o "+
"JOIN markets m ON o.market_id = m.id "+
"JOIN invoices i ON o.invoice_id = i.id "+
// QUESTION: Should unpaid orders contribute to quantity or not?
//
// The answer is relevant for concurrent orders:
// If they do, one can artificially increase the price for others
// by creating a lot of pending orders.
// If they don't, one can buy infinite amount of shares at the same price
// by creating a lot of small but concurrent orders.
// I think this means that pending order must be scoped to a user
// but this isn't sybil resistant.
//
// For now, we will ignore pending orders.
"WHERE o.market_id = $1 AND i.confirmed_at IS NOT NULL", id).Scan(
&l.Q1, &l.Q2); err != nil {
if err == sql.ErrNoRows {
return echo.NewHTTPError(http.StatusNotFound)
}
return err
}
return pages.Market(m, types.MarketP{
Pyes: lmsr.Price(l.B, l.Q2, l.Q1),
Pno: lmsr.Price(l.B, l.Q1, l.Q2), // prices
}).Render(context.RenderContext(sc, c), c.Response().Writer)
}
}
func GetPrice(sc context.Context) echo.HandlerFunc {
return func(c echo.Context) error {
var (
db = sc.Db
ctx = c.Request().Context()
id = c.Param("id")
m = types.Market{}
u = types.User{}
err error
)
if err = db.QueryRowContext(ctx, ""+
"SELECT m.id, m.question, m.description, m.created_at, m.end_date, "+
"u.id, u.name, u.created_at, u.ln_pubkey, u.nostr_pubkey, msats "+
"FROM markets m JOIN users u ON m.user_id = u.id "+
"WHERE m.id = $1", id).Scan(
&m.Id, &m.Question, &m.Description, &m.CreatedAt, &m.EndDate,
&u.Id, &u.Name, &u.CreatedAt, &u.LnPubkey, &u.NostrPubkey, &u.Msats); err != nil {
if err == sql.ErrNoRows {
@ -114,6 +184,6 @@ func HandleMarket(sc context.Context) echo.HandlerFunc {
m.User = u
return pages.Market(m).Render(context.RenderContext(sc, c), c.Response().Writer)
return nil
}
}

View File

@ -4,11 +4,13 @@ import (
"git.ekzyis.com/ekzyis/delphi.market/server/router/pages/components"
"git.ekzyis.com/ekzyis/delphi.market/types"
"github.com/dustin/go-humanize"
"strconv"
"fmt"
)
// TODO: Add countdown? Use or at least show somewhere precise timestamps?
templ Market(m types.Market) {
templ Market(m types.Market, p types.MarketP) {
<html>
@components.Head()
<body class="container">
@ -33,4 +35,13 @@ templ Market(m types.Market) {
@components.Footer()
</body>
</html>
}
func formatPrice(p float64) string {
return fmt.Sprintf("%v msats", strconv.FormatInt(pToPrice(p), 10))
}
func pToPrice(p float64) int64 {
// 0.513 means 513 msats
return int64(p * 1e3)
}

View File

@ -37,3 +37,15 @@ type Market struct {
CreatedAt time.Time
EndDate time.Time
}
type LSMR struct {
B float64
Q1 int
Q2 int
}
type MarketP struct {
// probability of outcomes
Pyes float64
Pno float64
}