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,
|
||||
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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue