Compare commits
No commits in common. "215a9fb3b6e47280b291e20f3d90cedf1f99ecd3" and "607e1007d06c0c842329ea44391a53907eae75ae" have entirely different histories.
215a9fb3b6
...
607e1007d0
@ -42,19 +42,34 @@ 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),
|
||||
lmsr_b FLOAT NOT NULL
|
||||
invoice_id INTEGER NOT NULL UNIQUE REFERENCES invoices(id)
|
||||
);
|
||||
|
||||
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,
|
||||
market_id INTEGER NOT NULL REFERENCES markets(id),
|
||||
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),
|
||||
user_id INTEGER NOT NULL REFERENCES users(id),
|
||||
side ORDER_SIDE NOT NULL,
|
||||
quantity BIGINT NOT NULL,
|
||||
outcome INTEGER NOT NULL,
|
||||
invoice_id INTEGER REFERENCES invoices(id)
|
||||
price BIGINT NOT NULL,
|
||||
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(
|
||||
id SERIAL PRIMARY KEY,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
@ -14,8 +14,7 @@ services:
|
||||
- delphi:/var/lib/postgresql/data
|
||||
- ./db/init.sql:/docker-entrypoint-initdb.d/init.sql
|
||||
- ./db/schema.sql:/docker-entrypoint-initdb.d/schema.sql
|
||||
# for some reason this can't be mounted on first run
|
||||
- ./db/postgresql.conf:/var/lib/postgresql/data/postgresql.conf
|
||||
- ./postgresql.conf:/var/lib/postgresql/data/postgresql.conf # for some reason this can't be mounted on first run
|
||||
|
||||
volumes:
|
||||
delphi:
|
@ -1,51 +0,0 @@
|
||||
package lmsr
|
||||
|
||||
import "math"
|
||||
|
||||
// Robin Hanson's Logarithmic Market Scoring Rule (LMSR) market maker
|
||||
// https://mason.gmu.edu/~rhanson/mktscore.pdf
|
||||
|
||||
// "The market maker keeps track of how many shares have been purchased by
|
||||
// traders in total so far for each outcome: that is, the number of shares
|
||||
// outstanding for each outcome. Let q1 and q2 be the number (“quantity”)
|
||||
// of shares outstanding for each of the two outcomes. The market maker also
|
||||
// maintains a cost function C(q1,q2) which records how much money traders
|
||||
// have collectively spent so far, and depends only on the number of shares
|
||||
// outstanding, q1 and q2. For LMSR, the cost function is [this]:
|
||||
//
|
||||
// <see code>
|
||||
// ...
|
||||
//
|
||||
// The parameter “b” controls the maximum possible amount of money the market
|
||||
// maker can lose (which happens to be b*ln2 in the two-outcome case). The
|
||||
// larger “b” is, the more money the market maker can lose. But a larger “b”
|
||||
// also means the market has more liquidity or depth, meaning that traders can
|
||||
// buy more shares at or near the current price without causing massive price swings."
|
||||
//
|
||||
// -- David Pennock, http://blog.oddhead.com/2006/10/30/implementing-hansons-market-maker/
|
||||
func binaryLMSRcost(b float64, q1 float64, q2 float64) float64 {
|
||||
return b * math.Log(math.Pow(math.E, q1/b)+math.Pow(math.E, q2/b))
|
||||
}
|
||||
|
||||
func Quote(b float64, q1 int, q2 int, dq1 int) float64 {
|
||||
// q1 must always be the quantity of shares that are bought / sold.
|
||||
// If you want to change q2, you need to swap q1 and q2 as input arguments.
|
||||
fq1 := float64(q1)
|
||||
fq2 := float64(q2)
|
||||
fdq1 := float64(dq1)
|
||||
return binaryLMSRcost(b, fq1+fdq1, fq2) - binaryLMSRcost(b, fq1, fq2)
|
||||
}
|
||||
|
||||
func Price(b float64, q1 int, q2 int) float64 {
|
||||
//
|
||||
// q1 must always be the quantity of shares that are bought / sold.
|
||||
// If you want to change q2, you need to swap q1 and q2 as input arguments.
|
||||
//
|
||||
// An infinitesimal share would cost this much:
|
||||
//
|
||||
// p = e^{q1/b} / ( e^{q1/b} + e^{q2/b} )
|
||||
//
|
||||
// However, to find the price of buying one full share
|
||||
// we need to use the cost function.
|
||||
return Quote(b, q1, q2, 1)
|
||||
}
|
@ -71,7 +71,7 @@ func (lnd *LNDClient) CheckInvoices(db *db.DB) {
|
||||
}
|
||||
|
||||
if !inv.State.IsFinal() {
|
||||
log.Printf("invoice pending: %s expiry=%s", h, time.Until(expiresAt))
|
||||
log.Printf("invoice pending: %s %s", h, time.Until(expiresAt))
|
||||
continue
|
||||
}
|
||||
|
||||
|
@ -78,15 +78,6 @@
|
||||
padding: 0 0.25em;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
color: var(--muted);
|
||||
border-color: var(--muted);
|
||||
}
|
||||
|
||||
.htmx-request {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 24px
|
||||
}
|
||||
@ -96,10 +87,6 @@
|
||||
aspect-ratio: 560/315;
|
||||
}
|
||||
|
||||
input {
|
||||
color: var(--black);
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: var(--muted);
|
||||
}
|
||||
@ -124,7 +111,7 @@
|
||||
color: var(--fg-success);
|
||||
}
|
||||
|
||||
.neon.success:hover, .neon.success.active {
|
||||
.neon.success:hover {
|
||||
background-color: var(--fg-success);
|
||||
color: var(--white);
|
||||
}
|
||||
@ -134,7 +121,7 @@
|
||||
color: var(--fg-error);
|
||||
}
|
||||
|
||||
.neon.error:hover, .neon.error.active {
|
||||
.neon.error:hover {
|
||||
background-color: var(--fg-error);
|
||||
color: var(--white);
|
||||
}
|
||||
|
File diff suppressed because one or more lines are too long
@ -57,7 +57,6 @@ func HandleInvoice(sc context.Context) echo.HandlerFunc {
|
||||
|
||||
var (
|
||||
marketRegexp = regexp.MustCompile("^create market (?P<id>[0-9]+)$")
|
||||
orderRegexp = regexp.MustCompile("^create order [0-9]+ for market (?P<id>[0-9]+)$")
|
||||
)
|
||||
|
||||
func toRedirectUrl(description string) templ.SafeURL {
|
||||
@ -66,9 +65,5 @@ func toRedirectUrl(description string) templ.SafeURL {
|
||||
marketId := m[marketRegexp.SubexpIndex("id")]
|
||||
return templ.SafeURL(fmt.Sprintf("/market/%s", marketId))
|
||||
}
|
||||
if m = orderRegexp.FindStringSubmatch(description); m != nil {
|
||||
marketId := m[marketRegexp.SubexpIndex("id")]
|
||||
return templ.SafeURL(fmt.Sprintf("/market/%s", marketId))
|
||||
}
|
||||
return "/"
|
||||
}
|
||||
|
@ -3,12 +3,9 @@ package handler
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"math"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"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"
|
||||
@ -33,14 +30,7 @@ func HandleCreate(sc context.Context) echo.HandlerFunc {
|
||||
endDate = c.FormValue("end_date")
|
||||
hash lntypes.Hash
|
||||
paymentRequest string
|
||||
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) / 1000) / math.Log(2)
|
||||
|
||||
cost = lnwire.MilliSatoshi(1000e3)
|
||||
expiry = int64(600)
|
||||
expiresAt = time.Now().Add(time.Second * time.Duration(expiry))
|
||||
invoiceId int
|
||||
@ -72,10 +62,10 @@ func HandleCreate(sc context.Context) echo.HandlerFunc {
|
||||
}
|
||||
|
||||
if err = tx.QueryRowContext(ctx, ""+
|
||||
"INSERT INTO markets (question, description, end_date, user_id, invoice_id, lmsr_b) "+
|
||||
"VALUES ($1, $2, $3, $4, $5, $6) "+
|
||||
"INSERT INTO markets (question, description, end_date, user_id, invoice_id) "+
|
||||
"VALUES ($1, $2, $3, $4, $5) "+
|
||||
"RETURNING id",
|
||||
question, description, endDate, u.Id, invoiceId, b).Scan(&marketId); err != nil {
|
||||
question, description, endDate, u.Id, invoiceId).Scan(&marketId); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -99,224 +89,29 @@ func HandleCreate(sc context.Context) echo.HandlerFunc {
|
||||
func HandleMarket(sc context.Context) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
var (
|
||||
db = sc.Db
|
||||
ctx = c.Request().Context()
|
||||
u = types.User{}
|
||||
id = c.Param("id")
|
||||
quantity = c.QueryParam("q")
|
||||
q int64
|
||||
m = types.Market{}
|
||||
mU = types.User{}
|
||||
l = types.LMSR{}
|
||||
total float64
|
||||
quote0 = types.MarketQuote{}
|
||||
quote1 = types.MarketQuote{}
|
||||
uQ0 int
|
||||
uQ1 int
|
||||
err error
|
||||
db = sc.Db
|
||||
ctx = c.Request().Context()
|
||||
id = c.Param("id")
|
||||
m = types.Market{}
|
||||
u = types.User{}
|
||||
err error
|
||||
)
|
||||
|
||||
if c.Get("session") != nil {
|
||||
u = c.Get("session").(types.User)
|
||||
} else {
|
||||
u.Id = -1
|
||||
}
|
||||
|
||||
if quantity == "" {
|
||||
q = 1
|
||||
} else if q, err = strconv.ParseInt(quantity, 10, 64); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "q must be integer")
|
||||
}
|
||||
|
||||
if err = db.QueryRowContext(ctx, ""+
|
||||
"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,
|
||||
&mU.Id, &mU.Name, &mU.CreatedAt, &mU.LnPubkey, &mU.NostrPubkey, &mU.Msats); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return echo.NewHTTPError(http.StatusNotFound)
|
||||
}
|
||||
return err
|
||||
}
|
||||
m.User = mU
|
||||
|
||||
if err = db.QueryRowContext(ctx, ""+
|
||||
"SELECT "+
|
||||
"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 = 0 AND o.user_id = $2), 0) AS uq1, "+
|
||||
"COALESCE(SUM(o.quantity) FILTER(WHERE o.outcome = 1 AND o.user_id = $2), 0) AS uq2 "+
|
||||
"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, u.Id).Scan(
|
||||
&l.Q1, &l.Q2, &uQ0, &uQ1); err != nil {
|
||||
"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 {
|
||||
return echo.NewHTTPError(http.StatusNotFound)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
total = lmsr.Quote(l.B, l.Q1, l.Q2, int(q))
|
||||
quote0 = types.MarketQuote{
|
||||
Outcome: 0,
|
||||
AvgPrice: total / float64(q),
|
||||
TotalPrice: total,
|
||||
Reward: float64(q) - total,
|
||||
}
|
||||
m.User = u
|
||||
|
||||
total = lmsr.Quote(l.B, l.Q2, l.Q1, int(q))
|
||||
quote1 = types.MarketQuote{
|
||||
Outcome: 1,
|
||||
AvgPrice: total / float64(q),
|
||||
TotalPrice: total,
|
||||
Reward: float64(q) - total,
|
||||
}
|
||||
|
||||
return pages.Market(m, quote0, quote1, uQ0, uQ1).Render(context.RenderContext(sc, c), c.Response().Writer)
|
||||
}
|
||||
}
|
||||
|
||||
func HandleOrder(sc context.Context) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
var (
|
||||
db = sc.Db
|
||||
lnd = sc.Lnd
|
||||
tx *sql.Tx
|
||||
ctx = c.Request().Context()
|
||||
u = c.Get("session").(types.User)
|
||||
id = c.Param("id")
|
||||
quantity = c.FormValue("q")
|
||||
outcome = c.FormValue("o")
|
||||
q int64
|
||||
o int64
|
||||
m = types.Market{}
|
||||
mU = types.User{}
|
||||
l = types.LMSR{}
|
||||
totalF float64
|
||||
total int
|
||||
hash lntypes.Hash
|
||||
paymentRequest string
|
||||
expiry = int64(60)
|
||||
expiresAt = time.Now().Add(time.Second * time.Duration(expiry))
|
||||
invoiceId int
|
||||
invDescription string
|
||||
orderId int
|
||||
qr templ.Component
|
||||
err error
|
||||
)
|
||||
|
||||
if quantity == "" {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "q must be given")
|
||||
} else if q, err = strconv.ParseInt(quantity, 10, 64); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "q must be integer")
|
||||
}
|
||||
|
||||
if outcome == "" {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "o must be given")
|
||||
} else if o, err = strconv.ParseInt(outcome, 10, 64); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "o must be integer")
|
||||
}
|
||||
if o < 0 && o > 1 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "o must be 0 or 1")
|
||||
}
|
||||
|
||||
// TODO: refactor since this uses same queries as function above
|
||||
if err = db.QueryRowContext(ctx, ""+
|
||||
"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,
|
||||
&mU.Id, &mU.Name, &mU.CreatedAt, &mU.LnPubkey, &mU.NostrPubkey, &mU.Msats); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return echo.NewHTTPError(http.StatusNotFound)
|
||||
}
|
||||
return err
|
||||
}
|
||||
m.User = mU
|
||||
|
||||
if err = db.QueryRowContext(ctx, ""+
|
||||
"SELECT "+
|
||||
"COALESCE(SUM(o.quantity) FILTER(WHERE o.outcome = 0), 0) AS q1, "+
|
||||
"COALESCE(SUM(o.quantity) FILTER(WHERE o.outcome = 1), 0) AS q2 "+
|
||||
"FROM orders o "+
|
||||
"JOIN markets m ON o.market_id = m.id "+
|
||||
"JOIN invoices i ON o.invoice_id = i.id "+
|
||||
"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
|
||||
}
|
||||
|
||||
if o == 0 {
|
||||
totalF = lmsr.Quote(l.B, l.Q1, l.Q2, int(q))
|
||||
} else if o == 1 {
|
||||
totalF = lmsr.Quote(l.B, l.Q2, l.Q1, int(q))
|
||||
}
|
||||
|
||||
total = int(math.Round(totalF * 1000))
|
||||
|
||||
if tx, err = db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelReadCommitted}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if hash, paymentRequest, err = lnd.Client.AddInvoice(ctx,
|
||||
&invoicesrpc.AddInvoiceData{
|
||||
Value: lnwire.MilliSatoshi(total),
|
||||
Expiry: expiry,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = tx.QueryRowContext(ctx, ""+
|
||||
"INSERT INTO invoices (user_id, msats, hash, bolt11, expires_at) "+
|
||||
"VALUES ($1, $2, $3, $4, $5) "+
|
||||
"RETURNING id",
|
||||
u.Id, total, hash.String(), paymentRequest, expiresAt).Scan(&invoiceId); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = tx.QueryRowContext(ctx, ""+
|
||||
"INSERT INTO orders (market_id, user_id, quantity, outcome, invoice_id) "+
|
||||
"VALUES ($1, $2, $3, $4, $5) "+
|
||||
"RETURNING id",
|
||||
id, u.Id, q, o, invoiceId).Scan(&orderId); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
invDescription = fmt.Sprintf("create order %d for market %s", orderId, id)
|
||||
if _, err = tx.ExecContext(ctx, ""+
|
||||
"UPDATE invoices SET description = $1 WHERE id = $2",
|
||||
invDescription, invoiceId); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = tx.Commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
qr = components.Invoice(hash.String(), paymentRequest, total, int(expiry), false, toRedirectUrl(invDescription))
|
||||
|
||||
return components.Modal(qr).Render(context.RenderContext(sc, c), c.Response().Writer)
|
||||
return pages.Market(m).Render(context.RenderContext(sc, c), c.Response().Writer)
|
||||
}
|
||||
}
|
||||
|
@ -24,7 +24,6 @@ templ Head() {
|
||||
}'
|
||||
/>
|
||||
<script src="/js/htmx.js" integrity="sha384-Xh+GLLi0SMFPwtHQjT72aPG19QvKB8grnyRbYBNIdHWc2NkCrz65jlU7YrzO6qRp" crossorigin="anonymous"></script>
|
||||
<script defer src="/js/alpine.js" crossorigin="anonymous"></script>
|
||||
if ctx.Value(c.EnvContextKey) == "development" {
|
||||
<script defer src="/js/hotreload.js"></script>
|
||||
}
|
||||
|
@ -1,67 +0,0 @@
|
||||
package components
|
||||
|
||||
import (
|
||||
"git.ekzyis.com/ekzyis/delphi.market/types"
|
||||
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
templ MarketForm(m types.Market, outcome int, q types.MarketQuote, uQ int) {
|
||||
<form
|
||||
id={ formId(outcome) }
|
||||
autocomplete="off"
|
||||
class="grid grid-cols-2 gap-3"
|
||||
hx-post={ fmt.Sprintf("/market/%d/order", m.Id) }
|
||||
hx-target="#modal"
|
||||
hx-swap="outerHTML"
|
||||
hx-select="#modal"
|
||||
>
|
||||
<input type="hidden" name="o" value={ fmt.Sprint(outcome) } />
|
||||
<div class="none col-span-2 htmx-request" />
|
||||
<label for="p">avg price per share:</label>
|
||||
<div id="p">{formatPrice(q.AvgPrice)}</div>
|
||||
<label for="q">how many?</label>
|
||||
<input
|
||||
id={ inputId(outcome) }
|
||||
name="q"
|
||||
class="text-black px-1"
|
||||
type="number"
|
||||
autofocus
|
||||
hx-get={ fmt.Sprintf("/market/%d", m.Id) }
|
||||
hx-replace-url="true"
|
||||
hx-target={ fmt.Sprintf("#%s", formId(outcome)) }
|
||||
hx-swap="outerHTML"
|
||||
hx-select={ fmt.Sprintf("#%s", formId(outcome)) }
|
||||
hx-trigger="input changed delay:1s"
|
||||
hx-preserve
|
||||
hx-disabled-elt="next button"
|
||||
hx-indicator={ hxIndicator(outcome) }
|
||||
/>
|
||||
<label for="total">you pay:</label>
|
||||
<div id="total">{formatPrice(q.TotalPrice)}</div>
|
||||
<label for="reward">{ "if you win:" }</label>
|
||||
<div id="reward">+{formatPrice(q.Reward)}</div>
|
||||
<label for="uQ">you have:</label>
|
||||
<div id="uQ">{ fmt.Sprint(uQ) }</div>
|
||||
<button type="submit" class="col-span-2">submit</button>
|
||||
</form>
|
||||
}
|
||||
|
||||
func formId (outcome int) string {
|
||||
return fmt.Sprintf("outcome-%d-form", outcome)
|
||||
}
|
||||
|
||||
func inputId (outcome int) string {
|
||||
return fmt.Sprintf("outcome-%d-q", outcome)
|
||||
}
|
||||
|
||||
func hxIndicator (outcome int) string {
|
||||
return fmt.Sprintf(
|
||||
"#%s>#p, #%s>#total, #%s>#reward",
|
||||
formId(outcome), formId(outcome), formId(outcome))
|
||||
}
|
||||
|
||||
func formatPrice(p float64) string {
|
||||
return fmt.Sprintf("%v sats", strconv.FormatFloat(p, 'f', 3, 64))
|
||||
}
|
@ -8,13 +8,10 @@ import (
|
||||
|
||||
// TODO: Add countdown? Use or at least show somewhere precise timestamps?
|
||||
|
||||
templ Market(m types.Market, q0 types.MarketQuote, q1 types.MarketQuote, uQ0 int, uQ1 int) {
|
||||
templ Market(m types.Market) {
|
||||
<html>
|
||||
@components.Head()
|
||||
<body
|
||||
x-data="{ outcome: undefined }"
|
||||
class="container"
|
||||
hx-preserve>
|
||||
<body class="container">
|
||||
@components.Nav()
|
||||
<div id="content" class="flex flex-col">
|
||||
<small>
|
||||
@ -24,42 +21,16 @@ templ Market(m types.Market, q0 types.MarketQuote, q1 types.MarketQuote, uQ0 int
|
||||
<div class="text-center text-muted my-1">{humanize.Time(m.EndDate)}</div>
|
||||
<div class="text-center text-muted my-1"></div>
|
||||
<blockquote cite="ekzyis" class="p-4 mb-4 border-s-4 border-muted">
|
||||
if m.Description != "" {
|
||||
m.Description
|
||||
} else {
|
||||
<empty>
|
||||
}
|
||||
{ m.Description }
|
||||
<div class="text-muted text-right pt-4">― {m.User.Name}, { humanize.Time(m.CreatedAt) }</div>
|
||||
</blockquote>
|
||||
<div class="flex flex-col justify-center my-1">
|
||||
<div class="flex flex-row justify-center">
|
||||
<button
|
||||
class="neon success mx-1"
|
||||
x-on:click="outcome = outcome === 1 ? undefined : 1"
|
||||
:class="{ 'active' : outcome === 1 }"
|
||||
>
|
||||
BET YES
|
||||
</button>
|
||||
<button
|
||||
class="neon error mx-1"
|
||||
x-on:click="outcome = outcome === 0 ? undefined : 0"
|
||||
:class="{ 'active' : outcome === 0 }"
|
||||
>
|
||||
BET NO
|
||||
</button>
|
||||
</div>
|
||||
<div class="mx-auto my-5" x-show="outcome === 1">
|
||||
@components.MarketForm(m, 1, q1, uQ1)
|
||||
</div>
|
||||
<div class="mx-auto my-5" x-show="outcome === 0">
|
||||
@components.MarketForm(m, 0, q0, uQ0)
|
||||
</div>
|
||||
<div class="flex justify-center my-1">
|
||||
<button class="neon success mx-1">BET YES</button>
|
||||
<button class="neon error mx-1">BET NO</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@components.Modal(nil)
|
||||
@components.Footer()
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
|
||||
}
|
@ -17,7 +17,6 @@ func Init(e *echo.Echo, sc Context) {
|
||||
e.GET("/create", handler.HandleIndex(sc))
|
||||
e.POST("/create", handler.HandleCreate(sc), middleware.SessionGuard(sc))
|
||||
e.GET("/market/:id", handler.HandleMarket(sc))
|
||||
e.POST("/market/:id/order", handler.HandleOrder(sc), middleware.SessionGuard(sc))
|
||||
e.GET("/about", handler.HandleAbout(sc))
|
||||
|
||||
e.GET("/login", handler.HandleAuth(sc, "login"))
|
||||
|
@ -37,16 +37,3 @@ type Market struct {
|
||||
CreatedAt time.Time
|
||||
EndDate time.Time
|
||||
}
|
||||
|
||||
type LMSR struct {
|
||||
B float64
|
||||
Q1 int
|
||||
Q2 int
|
||||
}
|
||||
|
||||
type MarketQuote struct {
|
||||
Outcome int
|
||||
AvgPrice float64
|
||||
TotalPrice float64
|
||||
Reward float64
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user