Finish porting simulator into React (#1)

* Preview bid results; toggle bid type

* Code cleanup: move hooks to where they're used

* Extract header to separate component

* Fix & reactify according to James's review

* Remove unnecessary useMemo

* Hack Chartjs type

* Add some notes on DX Todos

* Move non-page elements to lib/
This commit is contained in:
Austin Chen 2021-12-08 08:30:29 -08:00 committed by GitHub
parent 47eba79d05
commit 634c0af85b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 166 additions and 131 deletions

View File

@ -11,3 +11,8 @@
Before committing, run `npm run format` to format your code.
Recommended: Use a [Prettier editor integration](https://prettier.io/docs/en/editors.html) to automatically format on save
## Developer Experience TODOs
- Automatically run prettier as code commit hook?
- Prevent git pushing if there are Typescript errors?

52
web/components/header.tsx Normal file
View File

@ -0,0 +1,52 @@
import { Popover } from '@headlessui/react'
import Link from 'next/link'
const navigation = [
{
name: 'About',
href: 'https://mantic.notion.site/About-Mantic-Markets-09bdde9044614e62a27477b4b1bf77ea',
},
{ name: 'Simulator', href: '/simulator' },
]
export function Header() {
return (
<Popover as="header" className="relative">
<div className="pt-6">
<nav
className="relative max-w-7xl mx-auto flex items-center justify-between px-4 sm:px-6 bg-dark-50"
aria-label="Global"
>
<div className="flex items-center flex-1">
<div className="flex items-center justify-between w-full md:w-auto">
<Link href="/">
<a className="inline-grid grid-flow-col align-items-center h-6 sm:h-10">
<img
className="w-auto h-6 sm:h-10 inline-block mr-3"
src="/logo-icon.svg"
/>
<span className="text-white font-major-mono lowercase sm:text-2xl my-auto">
Mantic Markets
</span>
</a>
</Link>
</div>
<div className="space-x-8 md:flex md:ml-16">
{navigation.map((item) => (
<Link href={item.href}>
<a
key={item.name}
className="text-base font-medium text-white hover:text-gray-300"
>
{item.name}
</a>
</Link>
))}
</div>
</div>
</nav>
</div>
</Popover>
)
}

View File

@ -1,57 +1,10 @@
import { Popover } from '@headlessui/react'
import { ConvertKitEmailForm } from './convert-kit-email-form'
const navigation = [
{
name: 'About',
href: 'https://mantic.notion.site/About-Mantic-Markets-09bdde9044614e62a27477b4b1bf77ea',
},
{ name: 'Simulator', href: 'https://simulator.mantic.markets' },
]
import { Header } from './header'
export const Hero = () => {
return (
<div className="relative overflow-hidden h-screen bg-world-trading bg-cover bg-gray-900">
{/* <div className="absolute w-full h-full overflow-hidden bg-world-trading bg-cover bg-gray-900 z--1" /> */}
<Popover as="header" className="relative">
<div className="pt-6">
<nav
className="relative max-w-7xl mx-auto flex items-center justify-between px-4 sm:px-6"
aria-label="Global"
>
<div className="flex items-center flex-1">
<div className="flex items-center justify-between w-full md:w-auto">
<a
href="#"
className="inline-grid grid-flow-col align-items-center h-6 sm:h-10"
>
<img
className="w-auto h-6 sm:h-10 inline-block mr-3"
src="/logo-icon.svg"
/>
<span className="text-white font-major-mono lowercase sm:text-2xl my-auto">
Mantic Markets
</span>
</a>
</div>
<div className="space-x-8 md:flex md:ml-16">
{navigation.map((item) => (
<a
key={item.name}
href={item.href}
className="text-base font-medium text-white hover:text-gray-300"
>
{item.name}
</a>
))}
</div>
</div>
</nav>
</div>
</Popover>
<Header />
<main>
<div className="pt-40 sm:pt-16 lg:pt-8 lg:pb-14 lg:overflow-hidden">
<div className="mx-auto max-w-7xl lg:px-8">

View File

@ -13,8 +13,8 @@ export type Entry = {
prob: number
}
export function makeEntries(bids: Bid[]): Entry[] {
const entries: Entry[] = []
function makeWeights(bids: Bid[]) {
const weights = []
let yesPot = 0
let noPot = 0
// First pass: calculate all the weights
@ -28,33 +28,36 @@ export function makeEntries(bids: Bid[]): Entry[] {
noPot += noBid
const prob = yesPot / (yesPot + noPot)
entries.push({
weights.push({
yesBid,
noBid,
yesWeight,
noWeight,
prob,
// To be filled in below
yesPayout: 0,
noPayout: 0,
yesReturn: 0,
noReturn: 0,
})
}
return weights
}
export function makeEntries(bids: Bid[]): Entry[] {
const YES_SEED = bids[0].yesBid
const NO_SEED = bids[0].noBid
const yesWeightsSum = entries.reduce((sum, entry) => sum + entry.yesWeight, 0)
const noWeightsSum = entries.reduce((sum, entry) => sum + entry.noWeight, 0)
const weights = makeWeights(bids)
const yesPot = weights.reduce((sum, { yesBid }) => sum + yesBid, 0)
const noPot = weights.reduce((sum, { noBid }) => sum + noBid, 0)
const yesWeightsSum = weights.reduce((sum, entry) => sum + entry.yesWeight, 0)
const noWeightsSum = weights.reduce((sum, entry) => sum + entry.noWeight, 0)
// Second pass: calculate all the payouts
for (const entry of entries) {
const { yesBid, noBid, yesWeight, noWeight } = entry
const entries: Entry[] = []
for (const weight of weights) {
const { yesBid, noBid, yesWeight, noWeight } = weight
// Payout: You get your initial bid back, as well as your share of the
// (noPot - seed) according to your yesWeight
entry.yesPayout = yesBid + (yesWeight / yesWeightsSum) * (noPot - NO_SEED)
entry.noPayout = noBid + (noWeight / noWeightsSum) * (yesPot - YES_SEED)
entry.yesReturn = (entry.yesPayout - yesBid) / yesBid
entry.noReturn = (entry.noPayout - noBid) / noBid
const yesPayout = yesBid + (yesWeight / yesWeightsSum) * (noPot - NO_SEED)
const noPayout = noBid + (noWeight / noWeightsSum) * (yesPot - YES_SEED)
const yesReturn = (yesPayout - yesBid) / yesBid
const noReturn = (noPayout - noBid) / noBid
entries.push({ ...weight, yesPayout, noPayout, yesReturn, noReturn })
}
return entries
}

View File

@ -1,4 +1,4 @@
import React, { Fragment, useEffect, useMemo, useState } from 'react'
import React, { useEffect, useMemo, useState } from 'react'
import {
CategoryScale,
Chart,
@ -11,8 +11,8 @@ import {
} from 'chart.js'
import { ChartData } from 'chart.js'
import { Line } from 'react-chartjs-2'
import { bids as sampleBids } from './sample-bids'
import { Entry, makeEntries } from './entries'
import { bids as sampleBids } from '../../lib/simulator/sample-bids'
import { Entry, makeEntries } from '../../lib/simulator/entries'
// Auto import doesn't work for some reason...
// So we manually register ChartJS components instead:
@ -26,97 +26,136 @@ Chart.register(
Legend
)
function toTable(entries: Entry[]) {
return entries.map((entry, i) => {
return (
<tr key={i}>
<th>{i + 1}</th>
{toRowStart(entry)}
{toRowEnd(entry)}
</tr>
)
})
function TableBody(props: { entries: Entry[] }) {
return (
<tbody>
{props.entries.map((entry, i) => (
<tr key={i}>
<th>{i + 1}</th>
<TableRowStart entry={entry} />
<TableRowEnd entry={entry} />
</tr>
))}
</tbody>
)
}
function toRowStart(entry: Entry) {
function TableRowStart(props: { entry: Entry }) {
const { entry } = props
if (entry.yesBid && entry.noBid) {
return (
<Fragment>
<>
<td>
<div className="badge">SEED</div>
</td>
<td>
{entry.yesBid} / {entry.noBid}
</td>
</Fragment>
</>
)
} else if (entry.yesBid) {
return (
<Fragment>
<>
<td>
<div className="badge badge-success">YES</div>
</td>
<td>{entry.yesBid}</td>
</Fragment>
</>
)
} else if (entry.noBid) {
} else {
return (
<Fragment>
<>
<td>
<div className="badge badge-error">NO</div>
</td>
<td>{entry.noBid}</td>
</Fragment>
</>
)
}
}
function toRowEnd(entry: Entry) {
if (!entry.yesBid && !entry.noBid) {
function TableRowEnd(props: { entry: Entry | null }) {
const { entry } = props
if (!entry) {
return (
<Fragment>
<>
<td>N/A</td>
<td>N/A</td>
<td>N/A</td>
<td>N/A</td>
</Fragment>
</>
)
} else if (entry.yesBid && entry.noBid) {
return (
<Fragment>
<>
<td>N/A</td>
<td>{entry.prob.toFixed(2)}</td>
<td>N/A</td>
<td>N/A</td>
</Fragment>
</>
)
} else if (entry.yesBid) {
return (
<Fragment>
<>
<td>{entry.yesWeight.toFixed(2)}</td>
<td>{entry.prob.toFixed(2)}</td>
<td>{entry.yesPayout.toFixed(2)}</td>
<td>{(entry.yesReturn * 100).toFixed(2)}%</td>
</Fragment>
</>
)
} else {
return (
<Fragment>
<>
<td>{entry.noWeight.toFixed(2)}</td>
<td>{entry.prob.toFixed(2)}</td>
<td>{entry.noPayout.toFixed(2)}</td>
<td>{(entry.noReturn * 100).toFixed(2)}%</td>
</Fragment>
</>
)
}
}
function newBidTable(
steps: number,
newBid: number,
setNewBid: (newBid: number) => void,
submitBid: () => void
) {
function NewBidTable(props: {
steps: number
bids: any[]
setSteps: (steps: number) => void
setBids: (bids: any[]) => void
}) {
const { steps, bids, setSteps, setBids } = props
// Prepare for new bids
const [newBid, setNewBid] = useState(0)
const [newBidType, setNewBidType] = useState('YES')
function makeBid(type: string, bid: number) {
return {
yesBid: type == 'YES' ? bid : 0,
noBid: type == 'YES' ? 0 : bid,
}
}
function submitBid() {
if (newBid <= 0) return
const bid = makeBid(newBidType, newBid)
bids.splice(steps, 0, bid)
setBids(bids)
setSteps(steps + 1)
setNewBid(0)
}
function toggleBidType() {
setNewBidType(newBidType === 'YES' ? 'NO' : 'YES')
}
const nextEntry = useMemo(() => {
if (newBid) {
const nextBid = makeBid(newBidType, newBid)
const fakeBids = [...bids.slice(0, steps), nextBid]
const entries = makeEntries(fakeBids)
return entries[entries.length - 1]
}
return null
}, [newBid, newBidType, steps])
return (
<table className="table table-compact my-8 w-full">
<thead>
@ -137,16 +176,20 @@ function newBidTable(
<td>
<div
className={
`badge clickable ` + ('YES' ? 'badge-success' : 'badge-ghost')
`badge hover:cursor-pointer ` +
(newBidType == 'YES' ? 'badge-success' : 'badge-ghost')
}
onClick={toggleBidType}
>
YES
</div>
<br />
<div
className={
`badge clickable ` + ('NO' ? 'badge-error' : 'badge-ghost')
`badge hover:cursor-pointer ` +
(newBidType == 'NO' ? 'badge-error' : 'badge-ghost')
}
onClick={toggleBidType}
>
NO
</div>
@ -166,7 +209,7 @@ function newBidTable(
onFocus={(e) => e.target.select()}
/>
</td>
{/* <EntryRow :entry="nextEntry" /> */}
<TableRowEnd entry={nextEntry} />
<td>
<button
className="btn btn-primary"
@ -191,8 +234,7 @@ export default function Simulator() {
() => makeEntries(bids.slice(0, steps)),
[bids, steps]
)
const probs = useMemo(() => entries.map((entry) => entry.prob), [entries])
const probs = entries.map((entry) => entry.prob)
// Set up chart
const [chartData, setChartData] = useState({ datasets: [] } as ChartData)
@ -210,26 +252,6 @@ export default function Simulator() {
})
}, [steps])
// Prepare for new bids
const [newBid, setNewBid] = useState(0)
const [newBidType, setNewBidType] = useState('YES')
function makeBid(type: string, bid: number) {
return {
yesBid: type == 'YES' ? bid : 0,
noBid: type == 'YES' ? 0 : bid,
}
}
function submitBid() {
if (newBid <= 0) return
const bid = makeBid(newBidType, newBid)
bids.splice(steps, 0, bid)
setBids(bids)
setSteps(steps + 1)
setNewBid(0)
}
return (
<div className="overflow-x-auto px-12 mt-8 text-center">
<div className="grid grid-cols-1 xl:grid-cols-2 gap-4">
@ -249,8 +271,7 @@ export default function Simulator() {
onChange={(e) => setSteps(parseInt(e.target.value))}
/>
{/* New bid table */}
{newBidTable(steps, newBid, setNewBid, submitBid)}
<NewBidTable {...{ steps, bids, setSteps, setBids }} />
{/* History of bids */}
<div className="overflow-x-auto">
@ -266,7 +287,8 @@ export default function Simulator() {
<th>Return</th>
</tr>
</thead>
<tbody>{toTable(entries)}</tbody>
<TableBody entries={entries} />
</table>
</div>
</div>
@ -277,7 +299,7 @@ export default function Simulator() {
Probability of
<div className="badge badge-success text-2xl h-8 w-18">YES</div>
</h1>
<Line data={chartData} height={200} />
<Line data={chartData as any} height={200} />
</div>
</div>
</div>