Compare commits

...

4 Commits

Author SHA1 Message Date
ekzyis b7a24b48fd Show pYes and volume in market row 2024-09-10 23:41:55 +02:00
ekzyis 70b5659dc8 Format CSS file 2024-09-10 23:41:08 +02:00
ekzyis 5d5c91399f Rename vars and add comments 2024-09-10 22:50:19 +02:00
ekzyis 03689add7e Rename type MarketPoint to Point 2024-09-10 22:32:45 +02:00
6 changed files with 155 additions and 54 deletions

View File

@ -108,6 +108,14 @@
color: var(--color); color: var(--color);
} }
.text-success {
color: var(--fg-success);
}
.text-error {
color: var(--fg-error);
}
.hitbox { .hitbox {
padding: 15px; padding: 15px;
margin: -15px; margin: -15px;
@ -124,7 +132,8 @@
color: var(--fg-success); color: var(--fg-success);
} }
.neon.success:hover, .neon.success.active { .neon.success:hover,
.neon.success.active {
background-color: var(--fg-success); background-color: var(--fg-success);
color: var(--white); color: var(--white);
} }
@ -134,7 +143,8 @@
color: var(--fg-error); color: var(--fg-error);
} }
.neon.error:hover, .neon.error.active { .neon.error:hover,
.neon.error.active {
background-color: var(--fg-error); background-color: var(--fg-error);
color: var(--white); color: var(--white);
} }

View File

@ -3,6 +3,7 @@ package handler
import ( import (
"database/sql" "database/sql"
"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/types" "git.ekzyis.com/ekzyis/delphi.market/types"
@ -15,16 +16,29 @@ func HandleIndex(sc context.Context) echo.HandlerFunc {
db = sc.Db db = sc.Db
ctx = c.Request().Context() ctx = c.Request().Context()
rows *sql.Rows rows *sql.Rows
err error
markets []types.Market markets []types.Market
err error
) )
if rows, err = db.QueryContext(ctx, ""+ if rows, err = db.QueryContext(ctx, ""+
"WITH lmsr AS ("+
" SELECT "+
" o.market_id, "+
" COALESCE(SUM(o.quantity) FILTER(WHERE o.outcome = 0), 0) AS q1, "+
" COALESCE(SUM(o.quantity) FILTER(WHERE o.outcome = 1), 0) AS q2, "+
" COALESCE(SUM(i.msats_received), 0) / 1000 AS volume "+
" FROM orders o "+
" JOIN invoices i ON o.invoice_id = i.id "+
" WHERE i.confirmed_at IS NOT NULL "+
" GROUP BY o.market_id"+
")"+
"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, l.q1, l.q2, l.volume, "+
"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 "+
"JOIN lmsr l ON m.id = l.market_id "+
"WHERE i.confirmed_at IS NOT NULL"); err != nil { "WHERE i.confirmed_at IS NOT NULL"); err != nil {
return err return err
} }
@ -32,12 +46,16 @@ func HandleIndex(sc context.Context) echo.HandlerFunc {
for rows.Next() { for rows.Next() {
var m types.Market var m types.Market
var u types.User var u types.User
var l types.LMSR
if err = rows.Scan( if err = rows.Scan(
&m.Id, &m.Question, &m.Description, &m.CreatedAt, &m.EndDate, &m.Id, &m.Question, &m.Description, &m.CreatedAt, &m.EndDate,
&l.B, &l.Q1, &l.Q2, &m.Volume,
&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 {
return err return err
} }
m.User = u m.User = u
m.Pyes = lmsr.Quote(l.B, l.Q2, l.Q1, 1)
markets = append(markets, m) markets = append(markets, m)
} }

View File

@ -101,27 +101,51 @@ func HandleMarket(sc context.Context) echo.HandlerFunc {
var ( var (
db = sc.Db db = sc.Db
ctx = c.Request().Context() ctx = c.Request().Context()
// session user
u = types.User{} u = types.User{}
// market id
id = c.Param("id") id = c.Param("id")
// quantity of shares user entered into form
quantity = c.QueryParam("q") quantity = c.QueryParam("q")
// quantity as number
q int64 q int64
// current market
m = types.Market{} m = types.Market{}
// market founder
mU = types.User{} mU = types.User{}
// market LMSR data
l = types.LMSR{} l = types.LMSR{}
// total price for current quantity of shares in sats
total float64 total float64
quote0 = types.MarketQuote{}
quote1 = types.MarketQuote{} // market quotes
uQ0 int quoteNo = types.MarketQuote{}
uQ1 int quoteYes = types.MarketQuote{}
// how many shares the user already holds
uQuantityNo int
uQuantityYes int
rows *sql.Rows rows *sql.Rows
p0 []types.MarketPoint
p1 []types.MarketPoint // chart data
lineNo []types.Point
lineYes []types.Point
err error err error
) )
if c.Get("session") != nil { if c.Get("session") != nil {
u = c.Get("session").(types.User) u = c.Get("session").(types.User)
} else { } else {
// unauthenticated user
u.Id = -1 u.Id = -1
} }
@ -149,6 +173,7 @@ func HandleMarket(sc context.Context) echo.HandlerFunc {
if err = db.QueryRowContext(ctx, ""+ if err = db.QueryRowContext(ctx, ""+
"SELECT "+ "SELECT "+
"COALESCE(SUM(i.msats_received), 0) / 1000 AS volume, "+
"COALESCE(SUM(o.quantity) FILTER(WHERE o.outcome = 0), 0) AS q1, "+ "COALESCE(SUM(o.quantity) FILTER(WHERE o.outcome = 0), 0) AS q1, "+
"COALESCE(SUM(o.quantity) FILTER(WHERE o.outcome = 1), 0) AS q2, "+ "COALESCE(SUM(o.quantity) FILTER(WHERE o.outcome = 1), 0) AS q2, "+
"COALESCE(SUM(o.quantity) FILTER(WHERE o.outcome = 0 AND o.user_id = $2), 0) AS uq1, "+ "COALESCE(SUM(o.quantity) FILTER(WHERE o.outcome = 0 AND o.user_id = $2), 0) AS uq1, "+
@ -167,14 +192,16 @@ func HandleMarket(sc context.Context) echo.HandlerFunc {
// but this isn't sybil resistant. // but this isn't sybil resistant.
// //
// For now, we will ignore pending orders. // For now, we will ignore pending orders.
"WHERE o.market_id = $1 AND i.confirmed_at IS NOT NULL", id, u.Id).Scan( "WHERE o.market_id = $1 AND i.confirmed_at IS NOT NULL", id, u.Id).
&l.Q1, &l.Q2, &uQ0, &uQ1); err != nil { Scan(&m.Volume, &l.Q1, &l.Q2, &uQuantityNo, &uQuantityYes); err != nil {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return echo.NewHTTPError(http.StatusNotFound) return echo.NewHTTPError(http.StatusNotFound)
} }
return err return err
} }
m.Pyes = lmsr.Quote(l.B, l.Q2, l.Q1, 1)
if rows, err = db.QueryContext(ctx, ""+ if rows, err = db.QueryContext(ctx, ""+
"SELECT created_at, quote(b, q0, q1, 1) AS p0, quote(b, q1, q0, 1) AS p1 "+ "SELECT created_at, quote(b, q0, q1, 1) AS p0, quote(b, q1, q0, 1) AS p1 "+
"FROM ( "+ "FROM ( "+
@ -203,12 +230,12 @@ func HandleMarket(sc context.Context) echo.HandlerFunc {
if err = rows.Scan(&createdAt, &_p0, &_p1); err != nil { if err = rows.Scan(&createdAt, &_p0, &_p1); err != nil {
return err return err
} }
p0 = append(p0, types.MarketPoint{X: createdAt, Y: _p0}) lineNo = append(lineNo, types.Point{X: createdAt, Y: _p0})
p1 = append(p1, types.MarketPoint{X: createdAt, Y: _p1}) lineYes = append(lineYes, types.Point{X: createdAt, Y: _p1})
} }
total = lmsr.Quote(l.B, l.Q1, l.Q2, int(q)) total = lmsr.Quote(l.B, l.Q1, l.Q2, int(q))
quote0 = types.MarketQuote{ quoteNo = types.MarketQuote{
Outcome: 0, Outcome: 0,
AvgPrice: total / float64(q), AvgPrice: total / float64(q),
TotalPrice: total, TotalPrice: total,
@ -216,7 +243,7 @@ func HandleMarket(sc context.Context) echo.HandlerFunc {
} }
total = lmsr.Quote(l.B, l.Q2, l.Q1, int(q)) total = lmsr.Quote(l.B, l.Q2, l.Q1, int(q))
quote1 = types.MarketQuote{ quoteYes = types.MarketQuote{
Outcome: 1, Outcome: 1,
AvgPrice: total / float64(q), AvgPrice: total / float64(q),
TotalPrice: total, TotalPrice: total,
@ -225,9 +252,9 @@ func HandleMarket(sc context.Context) echo.HandlerFunc {
return pages.Market( return pages.Market(
m, m,
p0, p1, lineNo, lineYes,
quote0, quote1, quoteNo, quoteYes,
uQ0, uQ1).Render(context.RenderContext(sc, c), c.Response().Writer) uQuantityNo, uQuantityYes).Render(context.RenderContext(sc, c), c.Response().Writer)
} }
} }
@ -239,24 +266,48 @@ func HandleOrder(sc context.Context) echo.HandlerFunc {
tx *sql.Tx tx *sql.Tx
ctx = c.Request().Context() ctx = c.Request().Context()
u = c.Get("session").(types.User) u = c.Get("session").(types.User)
// market id
id = c.Param("id") id = c.Param("id")
// how many shares user wants to buy
quantity = c.FormValue("q") quantity = c.FormValue("q")
outcome = c.FormValue("o") // quantity as number
q int64 q int64
// on which outcome user wants to bet
outcome = c.FormValue("o")
// outcome as id
o int64 o int64
// selected market
m = types.Market{} m = types.Market{}
// market founder
mU = types.User{} mU = types.User{}
// market LMSR data
l = types.LMSR{} l = types.LMSR{}
// total price as returned by LMSR for given quantity
totalF float64 totalF float64
// total rounded to msats
total int total int
// invoice data
hash lntypes.Hash hash lntypes.Hash
paymentRequest string paymentRequest string
expiry = int64(60) expiry = int64(60)
expiresAt = time.Now().Add(time.Second * time.Duration(expiry)) expiresAt = time.Now().Add(time.Second * time.Duration(expiry))
invoiceId int invoiceId int
invDescription string invDescription string
// id of created order
orderId int orderId int
// QR component during render
qr templ.Component qr templ.Component
err error err error
) )

View File

@ -36,8 +36,8 @@ templ Index(markets []types.Market) {
<a href={ templ.SafeURL(fmt.Sprintf("/market/%d", m.Id)) }>{ m.Question }</a> <a href={ templ.SafeURL(fmt.Sprintf("/market/%d", m.Id)) }>{ m.Question }</a>
<div class="text-small text-muted">{ m.User.Name } / { humanize.Time(m.CreatedAt) } / { humanize.Time(m.EndDate) }</div> <div class="text-small text-muted">{ m.User.Name } / { humanize.Time(m.CreatedAt) } / { humanize.Time(m.EndDate) }</div>
</span> </span>
<span class="px-3 border-b border-muted pb-3 mt-3 flex"><div class="self-center">51%</div></span> <span class={ fmt.Sprintf("%s %s", "px-3 border-b border-muted pb-3 mt-3 flex", colorize(m.Pyes)) }><div class="self-center">{ fmt.Sprintf("%.2f%%", m.Pyes * 100) }</div></span>
<span class="pe-3 border-b border-muted pb-3 mt-3 flex"><div class="self-center">0</div></span> <span class="pe-3 border-b border-muted pb-3 mt-3 flex"><div class="self-center">{ fmt.Sprintf("%s", humanizeRound(m.Volume)) }</div></span>
} }
</div> </div>
} else { } else {
@ -92,6 +92,25 @@ func minDate() string {
return time.Now().Add(24 * time.Hour).Format("2006-01-02") return time.Now().Add(24 * time.Hour).Format("2006-01-02")
} }
func humanizeRound(f float64) string {
if f > 1_000_000 {
return fmt.Sprintf("%.2fm", f/1_000_000)
} else if f > 1_000 {
return fmt.Sprintf("%.2fk", f/1_000)
} else {
return fmt.Sprintf("%.2f", f)
}
}
func colorize(f float64) string {
if f > 0.5 {
return "text-success"
} else {
return "text-error"
}
}
func tabStyle(path string, tab string) string { func tabStyle(path string, tab string) string {
class := "!no-underline" class := "!no-underline"
if path == tab { if path == tab {

View File

@ -7,7 +7,7 @@ import (
) )
// 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, p0 []types.MarketPoint, p1 []types.MarketPoint, q0 types.MarketQuote, q1 types.MarketQuote, uQ0 int, uQ1 int) { templ Market(m types.Market, p0 []types.Point, p1 []types.Point, quoteNo types.MarketQuote, quoteYes types.MarketQuote, uQuantityNo int, uQuantityYes int) {
<html> <html>
@components.Head() @components.Head()
<body <body
@ -54,10 +54,10 @@ templ Market(m types.Market, p0 []types.MarketPoint, p1 []types.MarketPoint, q0
</button> </button>
</div> </div>
<div class="mx-auto my-5" x-show="outcome === 1"> <div class="mx-auto my-5" x-show="outcome === 1">
@components.MarketForm(m, 1, q1, uQ1) @components.MarketForm(m, 1, quoteYes, uQuantityYes)
</div> </div>
<div class="mx-auto my-5" x-show="outcome === 0"> <div class="mx-auto my-5" x-show="outcome === 0">
@components.MarketForm(m, 0, q0, uQ0) @components.MarketForm(m, 0, quoteNo, uQuantityNo)
</div> </div>
</div> </div>
</div> </div>

View File

@ -36,6 +36,9 @@ type Market struct {
Description string Description string
CreatedAt time.Time CreatedAt time.Time
EndDate time.Time EndDate time.Time
Pyes float64
// market volume in sats
Volume float64
} }
type LMSR struct { type LMSR struct {
@ -51,7 +54,7 @@ type MarketQuote struct {
Reward float64 Reward float64
} }
type MarketPoint struct { type Point struct {
X time.Time X time.Time
Y float64 Y float64
} }