Skip to content

Commit 6fb615a

Browse files
committed
Merge branch 'dev' of https://github.com/uzh-bf/gbl-uzh into jakob/report-improvements
# Conflicts: # apps/demo-game/src/pages/admin/reports/[id].tsx
2 parents 8467e92 + 42d2e19 commit 6fb615a

File tree

8 files changed

+306
-17
lines changed

8 files changed

+306
-17
lines changed

apps/demo-game/src/lib/analysis.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { standardDeviation } from '@gbl-uzh/platform/dist/lib/util'
12
import { PlayerResult } from 'src/graphql/generated/ops'
23
import { MONTHS, NUM_MONTHS } from './constants'
34

@@ -28,3 +29,33 @@ export const composeChartData = (dataPerPeriod: any, key: string) => {
2829
})
2930
return output
3031
}
32+
33+
export const computeRiskAndReturnOfPlayer = (
34+
segmentEndResultsOfPlayer: PlayerResult[]
35+
) => {
36+
const totalAssetsReturns = segmentEndResultsOfPlayer.flatMap(({ facts }) => {
37+
const assetsWithReturns = facts?.assetsWithReturns.slice(1) || []
38+
return assetsWithReturns.map(({ totalAssetsReturn }) => totalAssetsReturn)
39+
})
40+
41+
// TODO(JJ): 12 should rather be num_segments * 3 (because a segment is fixed as 3)
42+
const bankReturnPA: number =
43+
12 * segmentEndResultsOfPlayer?.[0]?.facts.assetsWithReturns[1].bankReturn
44+
45+
const num = totalAssetsReturns.length
46+
47+
const numResults = segmentEndResultsOfPlayer.length
48+
const lastResult = segmentEndResultsOfPlayer[numResults - 1]
49+
const assetsWithReturns = lastResult.facts?.assetsWithReturns
50+
const lastAccReturn = assetsWithReturns.slice(-1)[0].accTotalAssetsReturn
51+
52+
const risk = standardDeviation(totalAssetsReturns) * Math.sqrt(12)
53+
const returns = Math.pow(1 + lastAccReturn, 12 / num) - 1
54+
const sharpeRatio =
55+
risk > 0.0001 ? (returns - bankReturnPA) / risk : undefined
56+
return {
57+
returns,
58+
risk,
59+
sharpeRatio,
60+
}
61+
}

apps/demo-game/src/pages/admin/reports/[id].tsx

Lines changed: 207 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ import {
4141
Line,
4242
LineChart,
4343
ReferenceLine,
44+
Scatter,
45+
ScatterChart,
4446
XAxis,
4547
YAxis,
4648
} from 'recharts'
@@ -93,6 +95,20 @@ function ReportGame() {
9395
fetchPolicy: 'cache-first',
9496
})
9597

98+
const {
99+
data: periodEndResults,
100+
loading: periodEndResultsLoading,
101+
error: periodEndResultsError,
102+
} = useQuery(SpecificResultsDocument, {
103+
variables: {
104+
gameId: Number(router.query.id),
105+
type: 'PERIOD_END',
106+
},
107+
// pollInterval: 15000,
108+
skip: !router.query.id,
109+
fetchPolicy: 'cache-first',
110+
})
111+
96112
const memoizedData = useMemo(() => {
97113
if (
98114
loading ||
@@ -171,10 +187,11 @@ function ReportGame() {
171187
decisions[v] = Number(result.facts.decisions[v])
172188
})
173189

174-
const totalAssetsTmp = result.facts.assetsWithReturns
190+
const assetsWithReturns = result.facts.assetsWithReturns ?? []
191+
const totalAssetsTmp = assetsWithReturns
175192
.filter((_, ix) => ix > 0)
176193
.map((a) => a.totalAssets)
177-
const accTotalAssetsReturnTmp = result.facts.assetsWithReturns
194+
const accTotalAssetsReturnTmp = assetsWithReturns
178195
.filter((_, ix) => ix > 0)
179196
.map((a) => a.accTotalAssetsReturn)
180197

@@ -184,13 +201,22 @@ function ReportGame() {
184201
name: result.player.name,
185202
totalAssets: [...totalAssetsTmp],
186203
accTotalAssetsReturn: [...accTotalAssetsReturnTmp],
204+
risk: result.facts.risk,
205+
totalAssetsReturnsPA: result.facts.totalAssetsReturnsPA,
187206
}
188207
} else {
189208
dataPerPlayer[result.player.id].decisions.push(decisions)
190209
dataPerPlayer[result.player.id].totalAssets.push(...totalAssetsTmp)
191210
dataPerPlayer[result.player.id].accTotalAssetsReturn.push(
192211
...accTotalAssetsReturnTmp
193212
)
213+
if (result.facts.risk) {
214+
dataPerPlayer[result.player.id].risk = result.facts.risk
215+
}
216+
if (result.facts.totalAssetsReturnsPA) {
217+
dataPerPlayer[result.player.id].totalAssetsReturnsPA =
218+
result.facts.totalAssetsReturnsPA
219+
}
194220
}
195221
})
196222
output.push(dataPerPlayer)
@@ -316,6 +342,7 @@ function ReportGame() {
316342
dataTotalAssets,
317343
dataAccTotalAssetsReturn,
318344
segmentResultPerSegmentAvg,
345+
playerConfig,
319346
}
320347
}, [
321348
data,
@@ -326,7 +353,68 @@ function ReportGame() {
326353
segmentEndResultsError,
327354
])
328355

329-
if (loading || segmentEndResultsLoading || !memoizedData) {
356+
const memoizedDataPeriod = useMemo(() => {
357+
if (
358+
periodEndResultsLoading ||
359+
periodEndResultsError ||
360+
!periodEndResults?.specificResults
361+
) {
362+
return null
363+
}
364+
365+
const previousPeriodResults = periodEndResults.specificResults
366+
367+
console.log('previousPeriodResults', previousPeriodResults)
368+
369+
const riskReturnPerPeriod = previousPeriodResults.reduce((acc, result) => {
370+
if (!acc[result.period.index]) {
371+
acc[result.period.index] = {
372+
[result.player.id]: {
373+
name: result.player.name,
374+
risk: result.facts.risk,
375+
totalAssetsReturnsPA: result.facts.totalAssetsReturnsPA,
376+
},
377+
}
378+
} else {
379+
acc[result.period.index][result.player.id] = {
380+
name: result.player.name,
381+
risk: result.facts.risk,
382+
totalAssetsReturnsPA: result.facts.totalAssetsReturnsPA,
383+
}
384+
}
385+
return acc
386+
}, [])
387+
388+
const sharpeRatioPerPeriod = previousPeriodResults.reduce((acc, result) => {
389+
acc[result.period.index] = {
390+
...acc[result.period.index],
391+
period: result.period.index + 1,
392+
[result.player.name + '-sharpeRatio']: result.facts.sharpeRatio,
393+
}
394+
return acc
395+
}, [])
396+
397+
const configSharpeRatio = Object.keys(
398+
sharpeRatioPerPeriod?.[0] ?? {}
399+
).reduce((acc, item) => {
400+
if (item.endsWith('sharpeRatio')) {
401+
acc[item] = {
402+
label: item.replace('-sharpeRatio', ''),
403+
}
404+
}
405+
return acc
406+
}, {})
407+
408+
return { riskReturnPerPeriod, sharpeRatioPerPeriod, configSharpeRatio }
409+
}, [periodEndResults, periodEndResultsLoading, periodEndResultsError])
410+
411+
if (
412+
loading ||
413+
segmentEndResultsLoading ||
414+
periodEndResultsLoading ||
415+
!memoizedDataPeriod ||
416+
!memoizedData
417+
) {
330418
return <div>loading...</div>
331419
}
332420

@@ -336,6 +424,9 @@ function ReportGame() {
336424
if (segmentEndResultsError) {
337425
return <div>{segmentEndResultsError.message}</div>
338426
}
427+
if (periodEndResultsError) {
428+
return <div>{periodEndResultsError.message}</div>
429+
}
339430

340431
const {
341432
game,
@@ -346,6 +437,7 @@ function ReportGame() {
346437
dataTotalAssets,
347438
dataAccTotalAssetsReturn,
348439
segmentResultPerSegmentAvg,
440+
playerConfig,
349441
} = memoizedData
350442

351443
const decisionKeys = ['bank', 'bonds', 'stocks']
@@ -361,6 +453,9 @@ function ReportGame() {
361453
}
362454
})
363455

456+
const { riskReturnPerPeriod, sharpeRatioPerPeriod, configSharpeRatio } =
457+
memoizedDataPeriod
458+
364459
return (
365460
<div className="container mx-auto p-4">
366461
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-2">
@@ -682,6 +777,115 @@ function ReportGame() {
682777
</ChartContainer>
683778
</CardContent>
684779
</Card>
780+
781+
<Card className="flex h-full w-full flex-col">
782+
<CardHeader>
783+
<CardTitle>Risk-Return </CardTitle>
784+
<CardDescription>
785+
Risk-Return chart of the latest completed period.
786+
</CardDescription>
787+
</CardHeader>
788+
<CardContent className="flex-grow">
789+
<ChartContainer config={playerConfig} className="h-[300px] w-full">
790+
<ScatterChart>
791+
<ChartTooltip
792+
cursor={false}
793+
content={<ChartTooltipContent />}
794+
formatter={(value, name, item) => [
795+
<div
796+
key={name}
797+
className="flex w-full items-center justify-between gap-x-2"
798+
>
799+
<div className="flex items-center gap-x-1">
800+
<div
801+
className="h-[8px] w-[8px] rounded-sm"
802+
style={{ background: item.color }}
803+
/>
804+
<span className="text-xs text-gray-600">{name}</span>
805+
</div>
806+
<span className="font-bold text-black">
807+
{(value * 100).toFixed(2)}%
808+
</span>
809+
</div>,
810+
]}
811+
/>
812+
<CartesianGrid />
813+
<XAxis
814+
dataKey="risk"
815+
tickLine={false}
816+
tickMargin={8}
817+
type="number"
818+
name="Risk"
819+
tickFormatter={(v) => `${(v * 100).toFixed(2)}%`}
820+
/>
821+
<YAxis
822+
dataKey="totalAssetsReturnsPA"
823+
type="number"
824+
name="Returns p.a."
825+
tickLine={false}
826+
tickMargin={8}
827+
tickFormatter={(v) => `${(v * 100).toFixed(2)}%`}
828+
/>
829+
{Object.values(
830+
riskReturnPerPeriod?.[riskReturnPerPeriod.length - 1] ?? {}
831+
).map((playerData, ix) => {
832+
return (
833+
<Scatter
834+
key={playerData.name}
835+
name={playerData.name}
836+
data={[playerData]}
837+
fill={colors[ix]}
838+
/>
839+
)
840+
})}
841+
<ChartLegend content={<ChartLegendContent />} />
842+
</ScatterChart>
843+
</ChartContainer>
844+
</CardContent>
845+
</Card>
846+
847+
<Card className="flex h-full w-full flex-col">
848+
<CardHeader>
849+
<CardTitle>Sharpe Ratio</CardTitle>
850+
{/* <CardDescription>Average decisions over players.</CardDescription> */}
851+
</CardHeader>
852+
<CardContent className="flex-grow">
853+
<ChartContainer
854+
config={configSharpeRatio}
855+
className="h-[300px] w-full"
856+
>
857+
<BarChart data={sharpeRatioPerPeriod}>
858+
{Object.keys(configSharpeRatio).map((key, ix) => {
859+
return (
860+
<Bar key={key} dataKey={key} fill={colors[ix]} radius={4}>
861+
<LabelList
862+
position="top"
863+
className="fill-foreground"
864+
fontSize={12}
865+
formatter={(v) => `${v.toFixed(2)}`}
866+
/>
867+
</Bar>
868+
)
869+
})}
870+
<CartesianGrid vertical={false} />
871+
<XAxis
872+
dataKey="period"
873+
tickLine={false}
874+
axisLine={false}
875+
tickMargin={8}
876+
tickFormatter={(v) => `P${v}`}
877+
/>
878+
<YAxis
879+
tickLine={false}
880+
axisLine={false}
881+
tickMargin={8}
882+
tickFormatter={(v) => `${v.toFixed(2)}`}
883+
/>
884+
<ChartLegend content={<ChartLegendContent />} />
885+
</BarChart>
886+
</ChartContainer>
887+
</CardContent>
888+
</Card>
685889
</div>
686890
</div>
687891
)

apps/demo-game/src/services/PeriodResultService.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import {
55
} from '@gbl-uzh/platform'
66
import { debugLog } from '@gbl-uzh/platform/dist/lib/util'
77
import { produce } from 'immer'
8+
import { PlayerResult } from 'src/graphql/generated/ops'
9+
import { computeRiskAndReturnOfPlayer } from '../lib/analysis'
810
import { PlayerRole } from '../settings/Constants'
911
import { PeriodFacts, PeriodSegmentFacts } from '../types/Period'
1012
import { OutputResultFacts, ResultFacts, ResultFactsInit } from '../types/facts'
@@ -83,16 +85,31 @@ export function start(
8385

8486
export function end(
8587
facts: ResultFacts,
86-
payload: PayloadPeriodResultEnd<PeriodFacts, PeriodSegmentFacts, PlayerRole>
88+
payload: PayloadPeriodResultEnd<
89+
PlayerResult[],
90+
PeriodFacts,
91+
PeriodSegmentFacts,
92+
PlayerRole
93+
>
8794
): OutputResultFacts {
8895
const baseFacts: OutputResultFacts = {
8996
resultFacts: facts,
9097
events: [],
9198
}
9299

100+
const {
101+
returns: totalAssetsReturnsPA,
102+
risk,
103+
sharpeRatio,
104+
} = computeRiskAndReturnOfPlayer(payload.segmentEndResults)
105+
93106
const resultFacts: OutputResultFacts = produce(
94107
baseFacts,
95-
(draft: OutputResultFacts) => {}
108+
(draft: OutputResultFacts) => {
109+
draft.resultFacts.totalAssetsReturnsPA = totalAssetsReturnsPA
110+
draft.resultFacts.risk = risk
111+
draft.resultFacts.sharpeRatio = sharpeRatio
112+
}
96113
)
97114

98115
debugLog('PeriodResultEnd', facts, payload, resultFacts)

apps/demo-game/src/types/facts.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ export type ResultFactsInit = {
4343
export type ResultFacts = ResultFactsInit & {
4444
returns: Assets
4545
assetsWithReturns: AssetsWithReturns[]
46+
totalAssetsReturnsPA: number
47+
risk: number
48+
sharpeRatio?: number
4649
}
4750

4851
export type OutputResultFacts = OutputFacts<ResultFacts, any, any>

packages/platform/src/lib/util.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,14 @@ export function computeScenarioOutcome(
3434
return trend + (diceRoll - 7) * gap
3535
}
3636

37+
export function standardDeviation(arr: number[]) {
38+
const num = arr.length
39+
const mean = arr.reduce((acc, value) => acc + value, 0) / num
40+
const variance =
41+
arr.reduce((acc, value) => acc + Math.pow(value - mean, 2), 0) / (num - 1)
42+
return Math.sqrt(variance)
43+
}
44+
3745
export function computePercentChange(newValue: number, oldValue: number) {
3846
return (newValue - oldValue) / oldValue
3947
}

0 commit comments

Comments
 (0)