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:
parent
47eba79d05
commit
634c0af85b
|
@ -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
52
web/components/header.tsx
Normal 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>
|
||||
)
|
||||
}
|
|
@ -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">
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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) => {
|
||||
function TableBody(props: { entries: Entry[] }) {
|
||||
return (
|
||||
<tbody>
|
||||
{props.entries.map((entry, i) => (
|
||||
<tr key={i}>
|
||||
<th>{i + 1}</th>
|
||||
{toRowStart(entry)}
|
||||
{toRowEnd(entry)}
|
||||
<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>
|
||||
|
|
Loading…
Reference in New Issue
Block a user