manifold/web/pages/simulator/index.tsx

308 lines
7.3 KiB
TypeScript
Raw Normal View History

import React, { useEffect, useMemo, useState } from 'react'
2021-12-05 18:19:10 +00:00
import {
CategoryScale,
Chart,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
} from 'chart.js'
import { ChartData } from 'chart.js'
import { Line } from 'react-chartjs-2'
import { bids as sampleBids } from '../../lib/simulator/sample-bids'
import { Entry, makeEntries } from '../../lib/simulator/entries'
2021-12-05 18:19:10 +00:00
// Auto import doesn't work for some reason...
2021-12-06 20:03:12 +00:00
// So we manually register ChartJS components instead:
2021-12-05 18:19:10 +00:00
Chart.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend
)
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 TableRowStart(props: { entry: Entry }) {
const { entry } = props
if (entry.yesBid && entry.noBid) {
return (
<>
<td>
<div className="badge">SEED</div>
</td>
<td>
{entry.yesBid} / {entry.noBid}
</td>
</>
)
} else if (entry.yesBid) {
return (
<>
<td>
<div className="badge badge-success">YES</div>
</td>
<td>{entry.yesBid}</td>
</>
)
} else {
return (
<>
<td>
<div className="badge badge-error">NO</div>
</td>
<td>{entry.noBid}</td>
</>
)
}
}
function TableRowEnd(props: { entry: Entry | null }) {
const { entry } = props
if (!entry) {
return (
<>
<td>N/A</td>
<td>N/A</td>
<td>N/A</td>
<td>N/A</td>
</>
)
} else if (entry.yesBid && entry.noBid) {
return (
<>
<td>N/A</td>
<td>{entry.prob.toFixed(2)}</td>
<td>N/A</td>
<td>N/A</td>
</>
)
} else if (entry.yesBid) {
return (
<>
<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>
</>
)
} else {
return (
<>
<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>
</>
)
}
}
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])
2021-12-06 20:03:12 +00:00
return (
<table className="table table-compact my-8 w-full">
<thead>
<tr>
<th>Order #</th>
<th>Type</th>
<th>Bid</th>
<th>Weight</th>
<th>Probability</th>
<th>Payout</th>
<th>Return</th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<th>{steps + 1}</th>
<td>
<div
className={
`badge hover:cursor-pointer ` +
(newBidType == 'YES' ? 'badge-success' : 'badge-ghost')
2021-12-06 20:03:12 +00:00
}
onClick={toggleBidType}
2021-12-06 20:03:12 +00:00
>
YES
</div>
<br />
<div
className={
`badge hover:cursor-pointer ` +
(newBidType == 'NO' ? 'badge-error' : 'badge-ghost')
2021-12-06 20:03:12 +00:00
}
onClick={toggleBidType}
2021-12-06 20:03:12 +00:00
>
NO
</div>
</td>
<td>
<input
type="number"
placeholder="0"
className="input input-bordered"
value={newBid}
onChange={(e) => setNewBid(parseInt(e.target.value) || 0)}
onKeyUp={(e) => {
if (e.key === 'Enter') {
submitBid()
}
}}
onFocus={(e) => e.target.select()}
/>
</td>
<TableRowEnd entry={nextEntry} />
2021-12-06 20:03:12 +00:00
<td>
<button
className="btn btn-primary"
onClick={() => submitBid()}
disabled={newBid <= 0}
>
Submit
</button>
</td>
</tr>
</tbody>
</table>
)
}
// Show a hello world React page
export default function Simulator() {
const [steps, setSteps] = useState(10)
2021-12-06 20:03:12 +00:00
const [bids, setBids] = useState(sampleBids)
2021-12-06 20:03:12 +00:00
const entries = useMemo(
() => makeEntries(bids.slice(0, steps)),
[bids, steps]
)
const probs = entries.map((entry) => entry.prob)
2021-12-05 18:19:10 +00:00
2021-12-06 20:03:12 +00:00
// Set up chart
2021-12-05 18:19:10 +00:00
const [chartData, setChartData] = useState({ datasets: [] } as ChartData)
useEffect(() => {
setChartData({
labels: Array.from({ length: steps }, (_, i) => i + 1),
datasets: [
{
label: 'Implied probability',
data: probs,
borderColor: 'rgb(75, 192, 192)',
},
],
})
}, [steps])
return (
<div className="overflow-x-auto px-12 mt-8 text-center">
<div className="grid grid-cols-1 xl:grid-cols-2 gap-4">
2021-12-05 18:19:10 +00:00
{/* Left column */}
<div>
<h1 className="text-2xl font-bold text-gray-600 mb-8">
Dynamic Parimutuel Market Simulator
</h1>
{/* Range slider that sets the current step */}
<label>Simulation step: {steps}</label>
<input
type="range"
className="range"
min="1"
max={bids.length}
value={steps}
onChange={(e) => setSteps(parseInt(e.target.value))}
/>
<NewBidTable {...{ steps, bids, setSteps, setBids }} />
2021-12-06 20:03:12 +00:00
{/* History of bids */}
<div className="overflow-x-auto">
2021-12-05 18:19:10 +00:00
<table className="table w-full">
<thead>
<tr>
<th>Order #</th>
<th>Type</th>
<th>Bid</th>
<th>Weight</th>
2021-12-05 18:19:10 +00:00
<th>Prob</th>
<th>Max Payout</th>
<th>Return</th>
</tr>
</thead>
<TableBody entries={entries} />
</table>
</div>
</div>
2021-12-05 18:19:10 +00:00
{/* Right column */}
<div>
<h1 className="text-2xl font-bold text-gray-600 mb-8">
Probability of
<div className="badge badge-success text-2xl h-8 w-18">YES</div>
</h1>
<Line data={chartData as any} height={200} />
2021-12-05 18:19:10 +00:00
</div>
</div>
</div>
)
}