Calculate probabilities using LMSR
This commit is contained in:
parent
d87b97d235
commit
bd7b2715bd
|
@ -42,34 +42,19 @@ CREATE TABLE markets(
|
||||||
end_date TIMESTAMP WITH TIME ZONE NOT NULL,
|
end_date TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||||
settled_at TIMESTAMP WITH TIME ZONE,
|
settled_at TIMESTAMP WITH TIME ZONE,
|
||||||
user_id INTEGER NOT NULL REFERENCES users(id),
|
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(
|
CREATE TABLE orders(
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
market_id INTEGER NOT NULL REFERENCES markets(id),
|
||||||
deleted_at TIMESTAMP WITH TIME ZONE,
|
|
||||||
share_id INTEGER NOT NULL REFERENCES shares(id),
|
|
||||||
user_id INTEGER NOT NULL REFERENCES users(id),
|
user_id INTEGER NOT NULL REFERENCES users(id),
|
||||||
side ORDER_SIDE NOT NULL,
|
|
||||||
quantity BIGINT NOT NULL,
|
quantity BIGINT NOT NULL,
|
||||||
price BIGINT NOT NULL,
|
outcome INTEGER NOT NULL,
|
||||||
invoice_id INTEGER REFERENCES invoices(id),
|
invoice_id INTEGER REFERENCES invoices(id)
|
||||||
order_id INTEGER REFERENCES orders(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(
|
CREATE TABLE withdrawals(
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
|
@ -3,9 +3,11 @@ package handler
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"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/context"
|
||||||
"git.ekzyis.com/ekzyis/delphi.market/server/router/pages"
|
"git.ekzyis.com/ekzyis/delphi.market/server/router/pages"
|
||||||
"git.ekzyis.com/ekzyis/delphi.market/server/router/pages/components"
|
"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")
|
endDate = c.FormValue("end_date")
|
||||||
hash lntypes.Hash
|
hash lntypes.Hash
|
||||||
paymentRequest string
|
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)
|
expiry = int64(600)
|
||||||
expiresAt = time.Now().Add(time.Second * time.Duration(expiry))
|
expiresAt = time.Now().Add(time.Second * time.Duration(expiry))
|
||||||
invoiceId int
|
invoiceId int
|
||||||
|
@ -62,10 +71,10 @@ func HandleCreate(sc context.Context) echo.HandlerFunc {
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = tx.QueryRowContext(ctx, ""+
|
if err = tx.QueryRowContext(ctx, ""+
|
||||||
"INSERT INTO markets (question, description, end_date, user_id, invoice_id) "+
|
"INSERT INTO markets (question, description, end_date, user_id, invoice_id, lmsr_b) "+
|
||||||
"VALUES ($1, $2, $3, $4, $5) "+
|
"VALUES ($1, $2, $3, $4, $5, $6) "+
|
||||||
"RETURNING id",
|
"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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,16 +103,77 @@ func HandleMarket(sc context.Context) echo.HandlerFunc {
|
||||||
id = c.Param("id")
|
id = c.Param("id")
|
||||||
m = types.Market{}
|
m = types.Market{}
|
||||||
u = types.User{}
|
u = types.User{}
|
||||||
|
l = types.LSMR{}
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
|
|
||||||
if err = db.QueryRowContext(ctx, ""+
|
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 "+
|
"u.id, u.name, u.created_at, u.ln_pubkey, u.nostr_pubkey, u.msats "+
|
||||||
"FROM markets m "+
|
"FROM markets m "+
|
||||||
"JOIN users u ON m.user_id = u.id "+
|
"JOIN users u ON m.user_id = u.id "+
|
||||||
"JOIN invoices i ON m.invoice_id = i.id "+
|
"JOIN invoices i ON m.invoice_id = i.id "+
|
||||||
"WHERE m.id = $1 AND i.confirmed_at IS NOT NULL", id).Scan(
|
"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,
|
&m.Id, &m.Question, &m.Description, &m.CreatedAt, &m.EndDate,
|
||||||
&u.Id, &u.Name, &u.CreatedAt, &u.LnPubkey, &u.NostrPubkey, &u.Msats); err != nil {
|
&u.Id, &u.Name, &u.CreatedAt, &u.LnPubkey, &u.NostrPubkey, &u.Msats); err != nil {
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
|
@ -114,6 +184,6 @@ func HandleMarket(sc context.Context) echo.HandlerFunc {
|
||||||
|
|
||||||
m.User = u
|
m.User = u
|
||||||
|
|
||||||
return pages.Market(m).Render(context.RenderContext(sc, c), c.Response().Writer)
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,11 +4,13 @@ import (
|
||||||
"git.ekzyis.com/ekzyis/delphi.market/server/router/pages/components"
|
"git.ekzyis.com/ekzyis/delphi.market/server/router/pages/components"
|
||||||
"git.ekzyis.com/ekzyis/delphi.market/types"
|
"git.ekzyis.com/ekzyis/delphi.market/types"
|
||||||
"github.com/dustin/go-humanize"
|
"github.com/dustin/go-humanize"
|
||||||
|
"strconv"
|
||||||
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO: Add countdown? Use or at least show somewhere precise timestamps?
|
// 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>
|
<html>
|
||||||
@components.Head()
|
@components.Head()
|
||||||
<body class="container">
|
<body class="container">
|
||||||
|
@ -33,4 +35,13 @@ templ Market(m types.Market) {
|
||||||
@components.Footer()
|
@components.Footer()
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</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)
|
||||||
}
|
}
|
|
@ -37,3 +37,15 @@ type Market struct {
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
EndDate time.Time
|
EndDate time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LSMR struct {
|
||||||
|
B float64
|
||||||
|
Q1 int
|
||||||
|
Q2 int
|
||||||
|
}
|
||||||
|
|
||||||
|
type MarketP struct {
|
||||||
|
// probability of outcomes
|
||||||
|
Pyes float64
|
||||||
|
Pno float64
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue