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. 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 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' import { ConvertKitEmailForm } from './convert-kit-email-form'
import { Header } from './header'
const navigation = [
{
name: 'About',
href: 'https://mantic.notion.site/About-Mantic-Markets-09bdde9044614e62a27477b4b1bf77ea',
},
{ name: 'Simulator', href: 'https://simulator.mantic.markets' },
]
export const Hero = () => { export const Hero = () => {
return ( return (
<div className="relative overflow-hidden h-screen bg-world-trading bg-cover bg-gray-900"> <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" /> */} <Header />
<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>
<main> <main>
<div className="pt-40 sm:pt-16 lg:pt-8 lg:pb-14 lg:overflow-hidden"> <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"> <div className="mx-auto max-w-7xl lg:px-8">

View File

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

View File

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