├── src
├── vite-env.d.ts
├── components
│ ├── ScrollableCodeHighlight.module.css
│ ├── ScrollableCodeHighlight.tsx
│ └── ErrorAlert.tsx
├── utils
│ ├── colors.ts
│ ├── axios.ts
│ ├── format.ts
│ └── algorithm.tsx
├── main.tsx
├── hooks
│ ├── use-actual-color-scheme.ts
│ ├── use-color-scheme-state.ts
│ └── use-async.ts
├── pages
│ ├── home
│ │ ├── HomeCard.tsx
│ │ ├── AlgorithmList.tsx
│ │ ├── LoadFromUrl.tsx
│ │ ├── LoadFromFile.tsx
│ │ ├── AlgorithmDetail.tsx
│ │ ├── HomePage.tsx
│ │ └── LoadFromProsperity.tsx
│ ├── visualizer
│ │ ├── OrderDepthTableSpreadRow.tsx
│ │ ├── VisualizerCard.tsx
│ │ ├── ListingsTable.tsx
│ │ ├── SimpleTable.tsx
│ │ ├── PlainValueObservationsTable.tsx
│ │ ├── PositionTable.tsx
│ │ ├── PlainValueObservationChart.tsx
│ │ ├── OrdersTable.tsx
│ │ ├── ProfitLossTable.tsx
│ │ ├── EnvironmentChart.tsx
│ │ ├── TransportChart.tsx
│ │ ├── ProfitLossChart.tsx
│ │ ├── ConversionPriceChart.tsx
│ │ ├── VolumeChart.tsx
│ │ ├── TradesTable.tsx
│ │ ├── ConversionObservationsTable.tsx
│ │ ├── ProductPriceChart.tsx
│ │ ├── TimestampsCard.tsx
│ │ ├── PositionChart.tsx
│ │ ├── OrderDepthTable.tsx
│ │ ├── AlgorithmSummaryCard.tsx
│ │ ├── VisualizerPage.tsx
│ │ ├── TimestampDetail.tsx
│ │ └── Chart.tsx
│ └── base
│ │ ├── BasePage.tsx
│ │ ├── Header.module.css
│ │ ├── ColorSchemeSwitch.tsx
│ │ └── Header.tsx
├── store.ts
├── App.tsx
└── models.ts
├── .editorconfig
├── tsconfig.node.json
├── postcss.config.cjs
├── vite.config.ts
├── README.md
├── tsconfig.json
├── .github
└── workflows
│ └── build.yml
├── LICENSE
├── index.html
├── 404.html
├── eslint.config.js
├── package.json
└── .gitignore
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/src/components/ScrollableCodeHighlight.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | [data-scrollbars] {
3 | max-height: 300px;
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/utils/colors.ts:
--------------------------------------------------------------------------------
1 | export function getBidColor(alpha: number): string {
2 | return `rgba(39, 174, 96, ${alpha})`;
3 | }
4 |
5 | export function getAskColor(alpha: number): string {
6 | return `rgba(192, 57, 43, ${alpha})`;
7 | }
8 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | insert_final_newline = true
7 | trim_trailing_whitespace = true
8 |
9 | [*.md]
10 | indent_size = 4
11 | trim_trailing_whitespace = false
12 |
--------------------------------------------------------------------------------
/src/main.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import { App } from './App.tsx';
4 |
5 | ReactDOM.createRoot(document.getElementById('root')!).render(
6 |
7 |
8 | ,
9 | );
10 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "skipLibCheck": true,
5 | "module": "ESNext",
6 | "moduleResolution": "bundler",
7 | "allowSyntheticDefaultImports": true,
8 | "strict": true
9 | },
10 | "include": ["vite.config.ts"]
11 | }
12 |
--------------------------------------------------------------------------------
/src/components/ScrollableCodeHighlight.tsx:
--------------------------------------------------------------------------------
1 | import { CodeHighlight, CodeHighlightProps } from '@mantine/code-highlight';
2 | import { ReactNode } from 'react';
3 | import classes from './ScrollableCodeHighlight.module.css';
4 |
5 | export function ScrollableCodeHighlight(props: CodeHighlightProps): ReactNode {
6 | return ;
7 | }
8 |
--------------------------------------------------------------------------------
/src/utils/axios.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import { useStore } from '../store.ts';
3 |
4 | export const authenticatedAxios = axios.create();
5 |
6 | authenticatedAxios.interceptors.request.use(config => {
7 | const idToken = useStore.getState().idToken;
8 | if (idToken !== '') {
9 | config.headers.Authorization = `Bearer ${idToken}`;
10 | }
11 |
12 | return config;
13 | });
14 |
--------------------------------------------------------------------------------
/postcss.config.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | 'postcss-preset-mantine': {},
4 | 'postcss-simple-vars': {
5 | variables: {
6 | 'mantine-breakpoint-xs': '36em',
7 | 'mantine-breakpoint-sm': '48em',
8 | 'mantine-breakpoint-md': '62em',
9 | 'mantine-breakpoint-lg': '75em',
10 | 'mantine-breakpoint-xl': '88em',
11 | },
12 | },
13 | },
14 | };
15 |
--------------------------------------------------------------------------------
/src/hooks/use-actual-color-scheme.ts:
--------------------------------------------------------------------------------
1 | import { useMantineColorScheme } from '@mantine/core';
2 | import { useColorScheme } from '@mantine/hooks';
3 |
4 | export function useActualColorScheme(): 'light' | 'dark' {
5 | const mantineColorScheme = useMantineColorScheme();
6 | const systemColorScheme = useColorScheme();
7 |
8 | return mantineColorScheme.colorScheme === 'auto' ? systemColorScheme : mantineColorScheme.colorScheme;
9 | }
10 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import react from '@vitejs/plugin-react';
2 | import { defineConfig } from 'vite';
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | base: '/imc-prosperity-3-visualizer/',
8 | build: {
9 | minify: false,
10 | sourcemap: true,
11 | },
12 | resolve: {
13 | alias: {
14 | '@tabler/icons-react': '@tabler/icons-react/dist/esm/icons/index.mjs',
15 | },
16 | },
17 | });
18 |
--------------------------------------------------------------------------------
/src/utils/format.ts:
--------------------------------------------------------------------------------
1 | import { format } from 'date-fns';
2 |
3 | export function formatNumber(value: number, decimals: number = 0): string {
4 | return Number(value).toLocaleString(undefined, {
5 | minimumFractionDigits: decimals > 0 ? decimals : 0,
6 | maximumFractionDigits: decimals > 0 ? decimals : 0,
7 | });
8 | }
9 |
10 | export function formatTimestamp(timestamp: string): string {
11 | return format(Date.parse(timestamp), 'yyyy-MM-dd HH:mm:ss') + ' (local time)';
12 | }
13 |
--------------------------------------------------------------------------------
/src/pages/home/HomeCard.tsx:
--------------------------------------------------------------------------------
1 | import { Paper, Stack, Title } from '@mantine/core';
2 | import { ReactNode } from 'react';
3 |
4 | interface HomeCardProps {
5 | title: string;
6 | children: ReactNode;
7 | }
8 |
9 | export function HomeCard({ title, children }: HomeCardProps): ReactNode {
10 | return (
11 |
12 |
13 | {title}
14 |
15 |
16 | {children}
17 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/src/pages/visualizer/OrderDepthTableSpreadRow.tsx:
--------------------------------------------------------------------------------
1 | import { Table } from '@mantine/core';
2 | import { ReactNode } from 'react';
3 | import { formatNumber } from '../../utils/format.ts';
4 |
5 | export interface OrderDepthTableSpreadRowProps {
6 | spread: number;
7 | }
8 |
9 | export function OrderDepthTableSpreadRow({ spread }: OrderDepthTableSpreadRowProps): ReactNode {
10 | return (
11 |
12 |
13 | ↑ {formatNumber(spread)} ↓
14 |
15 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # IMC Prosperity 3 Visualizer
2 |
3 | [](https://github.com/jmerle/imc-prosperity-3-visualizer/actions/workflows/build.yml)
4 |
5 | This repository contains the source code behind [jmerle.github.io/imc-prosperity-3-visualizer/](https://jmerle.github.io/imc-prosperity-3-visualizer/), a visualizer for [IMC Prosperity 3](https://prosperity.imc.com/) algorithms. It is based on my visualizers for Prosperity [1](https://github.com/jmerle/imc-prosperity-visualizer) and [2](https://github.com/jmerle/imc-prosperity-2-visualizer).
6 |
--------------------------------------------------------------------------------
/src/pages/visualizer/VisualizerCard.tsx:
--------------------------------------------------------------------------------
1 | import { Paper, PaperProps, Title } from '@mantine/core';
2 | import { ReactNode } from 'react';
3 |
4 | interface VisualizerCardProps extends PaperProps {
5 | title?: string;
6 | children?: ReactNode;
7 | }
8 |
9 | export function VisualizerCard({ title, children, ...paperProps }: VisualizerCardProps): ReactNode {
10 | return (
11 |
12 | {title && (
13 |
14 | {title}
15 |
16 | )}
17 |
18 | {children}
19 |
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/src/pages/base/BasePage.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Center } from '@mantine/core';
2 | import { ReactNode, useEffect } from 'react';
3 | import { Outlet, useLocation } from 'react-router-dom';
4 | import { ColorSchemeSwitch } from './ColorSchemeSwitch.tsx';
5 | import { Header } from './Header.tsx';
6 |
7 | export function BasePage(): ReactNode {
8 | const { pathname } = useLocation();
9 |
10 | useEffect(() => {
11 | window.scrollTo(0, 0);
12 | }, [pathname]);
13 |
14 | return (
15 | <>
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | >
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "noEmit": true,
15 | "jsx": "react-jsx",
16 |
17 | /* Linting */
18 | "strict": true,
19 | "noUnusedLocals": true,
20 | "noUnusedParameters": true,
21 | "noFallthroughCasesInSwitch": true
22 | },
23 | "include": ["src"],
24 | "references": [{ "path": "./tsconfig.node.json" }]
25 | }
26 |
--------------------------------------------------------------------------------
/src/hooks/use-color-scheme-state.ts:
--------------------------------------------------------------------------------
1 | import { MantineColorScheme, useMantineColorScheme } from '@mantine/core';
2 | import { useCallback } from 'react';
3 | import { useStore } from '../store.ts';
4 |
5 | export function useColorSchemeState(): [MantineColorScheme, (colorScheme: MantineColorScheme) => void] {
6 | const storedColorScheme = useStore(state => state.colorScheme);
7 | const setStoredColorScheme = useStore(state => state.setColorScheme);
8 | const mantineColorScheme = useMantineColorScheme();
9 |
10 | const setColorScheme = useCallback(
11 | (value: string) => {
12 | setStoredColorScheme(value as MantineColorScheme);
13 | mantineColorScheme.setColorScheme(value as MantineColorScheme);
14 | },
15 | [setStoredColorScheme, mantineColorScheme],
16 | );
17 |
18 | return [storedColorScheme, setColorScheme];
19 | }
20 |
--------------------------------------------------------------------------------
/src/pages/home/AlgorithmList.tsx:
--------------------------------------------------------------------------------
1 | import { Accordion, Text } from '@mantine/core';
2 | import { ReactNode } from 'react';
3 | import { AlgorithmSummary } from '../../models.ts';
4 | import { AlgorithmDetail } from './AlgorithmDetail.tsx';
5 |
6 | export interface AlgorithmListProps {
7 | algorithms: AlgorithmSummary[];
8 | proxy: string;
9 | }
10 |
11 | export function AlgorithmList({ algorithms, proxy }: AlgorithmListProps): ReactNode {
12 | if (algorithms.length === 0) {
13 | return No algorithms found;
14 | }
15 |
16 | return (
17 |
18 | {algorithms.map((algorithm, i) => (
19 |
20 | ))}
21 |
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/src/pages/visualizer/ListingsTable.tsx:
--------------------------------------------------------------------------------
1 | import { Table } from '@mantine/core';
2 | import { ReactNode } from 'react';
3 | import { TradingState } from '../../models.ts';
4 | import { SimpleTable } from './SimpleTable.tsx';
5 |
6 | export interface ListingsTableProps {
7 | listings: TradingState['listings'];
8 | }
9 |
10 | export function ListingsTable({ listings }: ListingsTableProps): ReactNode {
11 | return (
12 | (
16 |
17 | {listing.symbol}
18 | {listing.product}
19 | {listing.denomination}
20 |
21 | ))}
22 | />
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/src/pages/visualizer/SimpleTable.tsx:
--------------------------------------------------------------------------------
1 | import { Table, Text } from '@mantine/core';
2 | import { ReactNode } from 'react';
3 |
4 | export interface SimpleTableProps {
5 | label: string;
6 | columns: string[];
7 | rows: ReactNode[];
8 | }
9 |
10 | export function SimpleTable({ label, columns, rows }: SimpleTableProps): ReactNode {
11 | if (rows.length === 0) {
12 | return Timestamp has no {label};
13 | }
14 |
15 | return (
16 |
17 |
18 |
19 |
20 | {columns.map((column, i) => (
21 | {column}
22 | ))}
23 |
24 |
25 | {rows}
26 |
27 |
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/src/pages/visualizer/PlainValueObservationsTable.tsx:
--------------------------------------------------------------------------------
1 | import { Table } from '@mantine/core';
2 | import { ReactNode } from 'react';
3 | import { TradingState } from '../../models.ts';
4 | import { formatNumber } from '../../utils/format.ts';
5 | import { SimpleTable } from './SimpleTable.tsx';
6 |
7 | export interface PlainValueObservationsTableProps {
8 | plainValueObservations: TradingState['observations']['plainValueObservations'];
9 | }
10 |
11 | export function PlainValueObservationsTable({ plainValueObservations }: PlainValueObservationsTableProps): ReactNode {
12 | const rows: ReactNode[] = [];
13 | for (const product of Object.keys(plainValueObservations)) {
14 | rows.push(
15 |
16 | {product}
17 | {formatNumber(plainValueObservations[product])}
18 | ,
19 | );
20 | }
21 |
22 | return ;
23 | }
24 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 |
9 | steps:
10 | - name: Checkout
11 | uses: actions/checkout@v4
12 |
13 | - name: Set up PNPM
14 | uses: pnpm/action-setup@v4
15 | with:
16 | version: 10
17 |
18 | - name: Set up Node.js
19 | uses: actions/setup-node@v4
20 | with:
21 | node-version: 22
22 | cache: pnpm
23 |
24 | - name: Install dependencies
25 | run: pnpm install --frozen-lockfile
26 |
27 | - name: Lint
28 | run: pnpm lint
29 |
30 | - name: Build
31 | run: pnpm build && cp 404.html dist/404.html
32 |
33 | - name: Deploy
34 | if: github.event_name == 'push' && github.ref == 'refs/heads/master'
35 | uses: JamesIves/github-pages-deploy-action@v4
36 | with:
37 | folder: dist
38 | git-config-name: GitHub Actions
39 | git-config-email: actions@github.com
40 |
--------------------------------------------------------------------------------
/src/pages/visualizer/PositionTable.tsx:
--------------------------------------------------------------------------------
1 | import { Table } from '@mantine/core';
2 | import { ReactNode } from 'react';
3 | import { TradingState } from '../../models.ts';
4 | import { getAskColor, getBidColor } from '../../utils/colors.ts';
5 | import { formatNumber } from '../../utils/format.ts';
6 | import { SimpleTable } from './SimpleTable.tsx';
7 |
8 | export interface PositionTableProps {
9 | position: TradingState['position'];
10 | }
11 |
12 | export function PositionTable({ position }: PositionTableProps): ReactNode {
13 | const rows: ReactNode[] = [];
14 | for (const product of Object.keys(position)) {
15 | if (position[product] === 0) {
16 | continue;
17 | }
18 |
19 | const colorFunc = position[product] > 0 ? getBidColor : getAskColor;
20 |
21 | rows.push(
22 |
23 | {product}
24 | {formatNumber(position[product])}
25 | ,
26 | );
27 | }
28 |
29 | return ;
30 | }
31 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Jasper van Merle
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/pages/visualizer/PlainValueObservationChart.tsx:
--------------------------------------------------------------------------------
1 | import Highcharts from 'highcharts';
2 | import { ReactNode } from 'react';
3 | import { ProsperitySymbol } from '../../models.ts';
4 | import { useStore } from '../../store.ts';
5 | import { Chart } from './Chart.tsx';
6 |
7 | export interface PlainValueObservationChartProps {
8 | symbol: ProsperitySymbol;
9 | }
10 |
11 | export function PlainValueObservationChart({ symbol }: PlainValueObservationChartProps): ReactNode {
12 | const algorithm = useStore(state => state.algorithm)!;
13 |
14 | const values = [];
15 |
16 | for (const row of algorithm.data) {
17 | const observation = row.state.observations.plainValueObservations[symbol];
18 | if (observation === undefined) {
19 | continue;
20 | }
21 |
22 | values.push([row.state.timestamp, observation]);
23 | }
24 |
25 | const options: Highcharts.Options = {
26 | yAxis: {
27 | allowDecimals: true,
28 | },
29 | };
30 |
31 | const series: Highcharts.SeriesOptionsType[] = [{ type: 'line', name: 'Value', data: values }];
32 |
33 | return ;
34 | }
35 |
--------------------------------------------------------------------------------
/src/pages/base/Header.module.css:
--------------------------------------------------------------------------------
1 | .header {
2 | height: rem(56px);
3 | margin-bottom: var(--mantine-spacing-md);
4 | background-color: var(--mantine-color-body);
5 | border-bottom: rem(1px) solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
6 | }
7 |
8 | .inner {
9 | height: rem(56px);
10 | display: flex;
11 | justify-content: space-between;
12 | align-items: center;
13 | }
14 |
15 | .link {
16 | display: block;
17 | line-height: 1;
18 | padding: rem(8px) rem(12px);
19 | border-radius: var(--mantine-radius-sm);
20 | text-decoration: none;
21 | color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-0));
22 | font-size: var(--mantine-font-size-sm);
23 | font-weight: 500;
24 |
25 | @mixin hover {
26 | background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
27 | }
28 |
29 | [data-mantine-color-scheme] &[data-active] {
30 | background-color: var(--mantine-color-blue-filled);
31 | color: var(--mantine-color-white);
32 | }
33 | }
34 |
35 | .linkDisabled {
36 | cursor: not-allowed;
37 | }
38 |
39 | .icon {
40 | vertical-align: top;
41 | padding-right: rem(2px);
42 | }
43 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | IMC Prosperity 3 Visualizer
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/src/store.ts:
--------------------------------------------------------------------------------
1 | import { MantineColorScheme } from '@mantine/core';
2 | import { create } from 'zustand';
3 | import { persist } from 'zustand/middleware';
4 | import { Algorithm } from './models.ts';
5 |
6 | export interface State {
7 | colorScheme: MantineColorScheme;
8 |
9 | idToken: string;
10 | round: string;
11 |
12 | algorithm: Algorithm | null;
13 |
14 | setColorScheme: (colorScheme: MantineColorScheme) => void;
15 | setIdToken: (idToken: string) => void;
16 | setRound: (round: string) => void;
17 | setAlgorithm: (algorithm: Algorithm | null) => void;
18 | }
19 |
20 | export const useStore = create()(
21 | persist(
22 | set => ({
23 | colorScheme: 'auto',
24 |
25 | idToken: '',
26 | round: 'ROUND0',
27 |
28 | algorithm: null,
29 |
30 | setColorScheme: colorScheme => set({ colorScheme }),
31 | setIdToken: idToken => set({ idToken }),
32 | setRound: round => set({ round }),
33 | setAlgorithm: algorithm => set({ algorithm }),
34 | }),
35 | {
36 | name: 'imc-prosperity-3-visualizer',
37 | partialize: state => ({
38 | colorScheme: state.colorScheme,
39 | idToken: state.idToken,
40 | round: state.round,
41 | }),
42 | },
43 | ),
44 | );
45 |
--------------------------------------------------------------------------------
/src/hooks/use-async.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useState } from 'react';
2 |
3 | export interface UseAsyncReturn Promise = any> {
4 | call: F;
5 | result: T | undefined;
6 | success: boolean;
7 | loading: boolean;
8 | error: Error | undefined;
9 | }
10 |
11 | export function useAsync Promise = any>(func: F): UseAsyncReturn {
12 | const [result, setResult] = useState();
13 | const [loading, setLoading] = useState(false);
14 | const [success, setSuccess] = useState(false);
15 | const [error, setError] = useState();
16 |
17 | const wrapper = useCallback(
18 | async (...args: any) => {
19 | setLoading(true);
20 | setResult(undefined);
21 | setSuccess(false);
22 | setError(undefined);
23 |
24 | try {
25 | const newResult = await func(...args);
26 |
27 | setResult(newResult);
28 | setSuccess(true);
29 |
30 | return newResult;
31 | } catch (err) {
32 | console.error(err);
33 | setError(err as Error);
34 | } finally {
35 | setLoading(false);
36 | }
37 | },
38 | [func],
39 | );
40 |
41 | return {
42 | call: wrapper as F,
43 | result,
44 | success,
45 | loading,
46 | error,
47 | };
48 | }
49 |
--------------------------------------------------------------------------------
/src/pages/visualizer/OrdersTable.tsx:
--------------------------------------------------------------------------------
1 | import { Table } from '@mantine/core';
2 | import { ReactNode } from 'react';
3 | import { AlgorithmDataRow } from '../../models.ts';
4 | import { getAskColor, getBidColor } from '../../utils/colors.ts';
5 | import { formatNumber } from '../../utils/format.ts';
6 | import { SimpleTable } from './SimpleTable.tsx';
7 |
8 | export interface OrdersTableProps {
9 | orders: AlgorithmDataRow['orders'];
10 | }
11 |
12 | export function OrdersTable({ orders }: OrdersTableProps): ReactNode {
13 | const rows: ReactNode[] = [];
14 | for (const symbol of Object.keys(orders)) {
15 | for (let i = 0; i < orders[symbol].length; i++) {
16 | const order = orders[symbol][i];
17 |
18 | const colorFunc = order.quantity > 0 ? getBidColor : getAskColor;
19 |
20 | rows.push(
21 |
22 | {order.symbol}
23 | {order.quantity > 0 ? 'Buy' : 'Sell'}
24 | {formatNumber(order.price)}
25 | {formatNumber(Math.abs(order.quantity))}
26 | ,
27 | );
28 | }
29 | }
30 |
31 | return ;
32 | }
33 |
--------------------------------------------------------------------------------
/src/pages/base/ColorSchemeSwitch.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Center, SegmentedControl } from '@mantine/core';
2 | import { IconDeviceDesktop, IconMoon, IconSun } from '@tabler/icons-react';
3 | import { ReactNode } from 'react';
4 | import { useColorSchemeState } from '../../hooks/use-color-scheme-state.ts';
5 |
6 | export function ColorSchemeSwitch(): ReactNode {
7 | const [colorScheme, setColorScheme] = useColorSchemeState();
8 |
9 | return (
10 | void}
14 | data={[
15 | {
16 | label: (
17 |
18 |
19 | Light
20 |
21 | ),
22 | value: 'light',
23 | },
24 | {
25 | label: (
26 |
27 |
28 | Dark
29 |
30 | ),
31 | value: 'dark',
32 | },
33 | {
34 | label: (
35 |
36 |
37 | System
38 |
39 | ),
40 | value: 'auto',
41 | },
42 | ]}
43 | />
44 | );
45 | }
46 |
--------------------------------------------------------------------------------
/src/pages/visualizer/ProfitLossTable.tsx:
--------------------------------------------------------------------------------
1 | import { Table } from '@mantine/core';
2 | import { ReactNode } from 'react';
3 | import { useStore } from '../../store.ts';
4 | import { getAskColor, getBidColor } from '../../utils/colors.ts';
5 | import { formatNumber } from '../../utils/format.ts';
6 | import { SimpleTable } from './SimpleTable.tsx';
7 |
8 | export interface ProfitLossTableProps {
9 | timestamp: number;
10 | }
11 |
12 | export function ProfitLossTable({ timestamp }: ProfitLossTableProps): ReactNode {
13 | const algorithm = useStore(state => state.algorithm)!;
14 |
15 | const rows: ReactNode[] = algorithm.activityLogs
16 | .filter(row => row.timestamp === timestamp)
17 | .sort((a, b) => a.product.localeCompare(b.product))
18 | .map(row => {
19 | let colorFunc: (alpha: number) => string = () => 'transparent';
20 | if (row.profitLoss > 0) {
21 | colorFunc = getBidColor;
22 | } else if (row.profitLoss < 0) {
23 | colorFunc = getAskColor;
24 | }
25 |
26 | return (
27 |
28 | {row.product}
29 | {formatNumber(row.profitLoss)}
30 |
31 | );
32 | });
33 |
34 | return ;
35 | }
36 |
--------------------------------------------------------------------------------
/src/pages/visualizer/EnvironmentChart.tsx:
--------------------------------------------------------------------------------
1 | import Highcharts from 'highcharts';
2 | import { ReactNode } from 'react';
3 | import { ProsperitySymbol } from '../../models.ts';
4 | import { useStore } from '../../store.ts';
5 | import { Chart } from './Chart.tsx';
6 |
7 | export interface EnvironmentChartProps {
8 | symbol: ProsperitySymbol;
9 | }
10 |
11 | export function EnvironmentChart({ symbol }: EnvironmentChartProps): ReactNode {
12 | const algorithm = useStore(state => state.algorithm)!;
13 |
14 | const sugarPriceData = [];
15 | const sunlightIndexData = [];
16 |
17 | for (const row of algorithm.data) {
18 | const observation = row.state.observations.conversionObservations[symbol];
19 | if (observation === undefined) {
20 | continue;
21 | }
22 |
23 | sugarPriceData.push([row.state.timestamp, observation.sugarPrice]);
24 | sunlightIndexData.push([row.state.timestamp, observation.sunlightIndex]);
25 | }
26 |
27 | const series: Highcharts.SeriesOptionsType[] = [
28 | { type: 'line', name: 'Sugar Price', marker: { symbol: 'square' }, yAxis: 0, data: sugarPriceData },
29 | { type: 'line', name: 'Sunlight Index', marker: { symbol: 'circle' }, yAxis: 1, data: sunlightIndexData },
30 | ];
31 |
32 | const options: Highcharts.Options = {
33 | yAxis: [{}, { opposite: true }],
34 | };
35 |
36 | return ;
37 | }
38 |
--------------------------------------------------------------------------------
/src/pages/visualizer/TransportChart.tsx:
--------------------------------------------------------------------------------
1 | import Highcharts from 'highcharts';
2 | import { ReactNode } from 'react';
3 | import { ProsperitySymbol } from '../../models.ts';
4 | import { useStore } from '../../store.ts';
5 | import { Chart } from './Chart.tsx';
6 |
7 | export interface TransportChartProps {
8 | symbol: ProsperitySymbol;
9 | }
10 |
11 | export function TransportChart({ symbol }: TransportChartProps): ReactNode {
12 | const algorithm = useStore(state => state.algorithm)!;
13 |
14 | const transportFeesData = [];
15 | const importTariffData = [];
16 | const exportTariffData = [];
17 |
18 | for (const row of algorithm.data) {
19 | const observation = row.state.observations.conversionObservations[symbol];
20 | if (observation === undefined) {
21 | continue;
22 | }
23 |
24 | transportFeesData.push([row.state.timestamp, observation.transportFees]);
25 | importTariffData.push([row.state.timestamp, observation.importTariff]);
26 | exportTariffData.push([row.state.timestamp, observation.exportTariff]);
27 | }
28 |
29 | const series: Highcharts.SeriesOptionsType[] = [
30 | { type: 'line', name: 'Transport fees', data: transportFeesData },
31 | { type: 'line', name: 'Import tariff', marker: { symbol: 'triangle' }, data: importTariffData },
32 | { type: 'line', name: 'Export tariff', marker: { symbol: 'triangle-down' }, data: exportTariffData },
33 | ];
34 |
35 | return ;
36 | }
37 |
--------------------------------------------------------------------------------
/src/pages/visualizer/ProfitLossChart.tsx:
--------------------------------------------------------------------------------
1 | import Highcharts from 'highcharts';
2 | import { ReactNode } from 'react';
3 | import { useStore } from '../../store.ts';
4 | import { Chart } from './Chart.tsx';
5 |
6 | export interface ProfitLossChartProps {
7 | symbols: string[];
8 | }
9 |
10 | export function ProfitLossChart({ symbols }: ProfitLossChartProps): ReactNode {
11 | const algorithm = useStore(state => state.algorithm)!;
12 |
13 | const dataByTimestamp = new Map();
14 | for (const row of algorithm.activityLogs) {
15 | if (!dataByTimestamp.has(row.timestamp)) {
16 | dataByTimestamp.set(row.timestamp, row.profitLoss);
17 | } else {
18 | dataByTimestamp.set(row.timestamp, dataByTimestamp.get(row.timestamp)! + row.profitLoss);
19 | }
20 | }
21 |
22 | const series: Highcharts.SeriesOptionsType[] = [
23 | {
24 | type: 'line',
25 | name: 'Total',
26 | data: [...dataByTimestamp.keys()].map(timestamp => [timestamp, dataByTimestamp.get(timestamp)]),
27 | },
28 | ];
29 |
30 | symbols.forEach(symbol => {
31 | const data = [];
32 |
33 | for (const row of algorithm.activityLogs) {
34 | if (row.product === symbol) {
35 | data.push([row.timestamp, row.profitLoss]);
36 | }
37 | }
38 |
39 | series.push({
40 | type: 'line',
41 | name: symbol,
42 | data,
43 | dashStyle: 'Dash',
44 | });
45 | });
46 |
47 | return ;
48 | }
49 |
--------------------------------------------------------------------------------
/src/pages/visualizer/ConversionPriceChart.tsx:
--------------------------------------------------------------------------------
1 | import Highcharts from 'highcharts';
2 | import { ReactNode } from 'react';
3 | import { ProsperitySymbol } from '../../models.ts';
4 | import { useStore } from '../../store.ts';
5 | import { getAskColor, getBidColor } from '../../utils/colors.ts';
6 | import { Chart } from './Chart.tsx';
7 |
8 | export interface ConversionPriceChartProps {
9 | symbol: ProsperitySymbol;
10 | }
11 |
12 | export function ConversionPriceChart({ symbol }: ConversionPriceChartProps): ReactNode {
13 | const algorithm = useStore(state => state.algorithm)!;
14 |
15 | const bidPriceData = [];
16 | const askPriceData = [];
17 |
18 | for (const row of algorithm.data) {
19 | const observation = row.state.observations.conversionObservations[symbol];
20 | if (observation === undefined) {
21 | continue;
22 | }
23 |
24 | bidPriceData.push([row.state.timestamp, observation.bidPrice]);
25 | askPriceData.push([row.state.timestamp, observation.askPrice]);
26 | }
27 |
28 | const options: Highcharts.Options = {
29 | yAxis: {
30 | opposite: true,
31 | allowDecimals: true,
32 | },
33 | };
34 |
35 | const series: Highcharts.SeriesOptionsType[] = [
36 | { type: 'line', name: 'Bid', color: getBidColor(1.0), marker: { symbol: 'triangle' }, data: bidPriceData },
37 | { type: 'line', name: 'Ask', color: getAskColor(1.0), marker: { symbol: 'triangle-down' }, data: askPriceData },
38 | ];
39 |
40 | return ;
41 | }
42 |
--------------------------------------------------------------------------------
/src/pages/visualizer/VolumeChart.tsx:
--------------------------------------------------------------------------------
1 | import Highcharts from 'highcharts';
2 | import { ReactNode } from 'react';
3 | import { ProsperitySymbol } from '../../models.ts';
4 | import { useStore } from '../../store.ts';
5 | import { getAskColor, getBidColor } from '../../utils/colors.ts';
6 | import { Chart } from './Chart.tsx';
7 |
8 | export interface VolumeChartProps {
9 | symbol: ProsperitySymbol;
10 | }
11 |
12 | export function VolumeChart({ symbol }: VolumeChartProps): ReactNode {
13 | const algorithm = useStore(state => state.algorithm)!;
14 |
15 | const series: Highcharts.SeriesOptionsType[] = [
16 | { type: 'column', name: 'Bid 3', color: getBidColor(0.5), data: [] },
17 | { type: 'column', name: 'Bid 2', color: getBidColor(0.75), data: [] },
18 | { type: 'column', name: 'Bid 1', color: getBidColor(1.0), data: [] },
19 | { type: 'column', name: 'Ask 1', color: getAskColor(1.0), data: [] },
20 | { type: 'column', name: 'Ask 2', color: getAskColor(0.75), data: [] },
21 | { type: 'column', name: 'Ask 3', color: getAskColor(0.5), data: [] },
22 | ];
23 |
24 | for (const row of algorithm.activityLogs) {
25 | if (row.product !== symbol) {
26 | continue;
27 | }
28 |
29 | for (let i = 0; i < row.bidVolumes.length; i++) {
30 | (series[2 - i] as any).data.push([row.timestamp, row.bidVolumes[i]]);
31 | }
32 |
33 | for (let i = 0; i < row.askVolumes.length; i++) {
34 | (series[i + 3] as any).data.push([row.timestamp, row.askVolumes[i]]);
35 | }
36 | }
37 |
38 | return ;
39 | }
40 |
--------------------------------------------------------------------------------
/src/pages/visualizer/TradesTable.tsx:
--------------------------------------------------------------------------------
1 | import { Table } from '@mantine/core';
2 | import { ReactNode } from 'react';
3 | import { ProsperitySymbol, Trade } from '../../models.ts';
4 | import { getAskColor, getBidColor } from '../../utils/colors.ts';
5 | import { formatNumber } from '../../utils/format.ts';
6 | import { SimpleTable } from './SimpleTable.tsx';
7 |
8 | export interface TradesTableProps {
9 | trades: Record;
10 | }
11 |
12 | export function TradesTable({ trades }: TradesTableProps): ReactNode {
13 | const rows: ReactNode[] = [];
14 | for (const symbol of Object.keys(trades).sort((a, b) => a.localeCompare(b))) {
15 | for (let i = 0; i < trades[symbol].length; i++) {
16 | const trade = trades[symbol][i];
17 |
18 | let color: string;
19 | if (trade.buyer === 'SUBMISSION') {
20 | color = getBidColor(0.1);
21 | } else if (trade.seller === 'SUBMISSION') {
22 | color = getAskColor(0.1);
23 | } else {
24 | color = 'transparent';
25 | }
26 |
27 | rows.push(
28 |
29 | {trade.symbol}
30 | {trade.buyer}
31 | {trade.seller}
32 | {formatNumber(trade.price)}
33 | {formatNumber(trade.quantity)}
34 | {formatNumber(trade.timestamp)}
35 | ,
36 | );
37 | }
38 | }
39 |
40 | return (
41 |
42 | );
43 | }
44 |
--------------------------------------------------------------------------------
/src/pages/visualizer/ConversionObservationsTable.tsx:
--------------------------------------------------------------------------------
1 | import { Table } from '@mantine/core';
2 | import { ReactNode } from 'react';
3 | import { TradingState } from '../../models.ts';
4 | import { formatNumber } from '../../utils/format.ts';
5 | import { SimpleTable } from './SimpleTable.tsx';
6 |
7 | export interface ConversionObservationsTableProps {
8 | conversionObservations: TradingState['observations']['conversionObservations'];
9 | }
10 |
11 | export function ConversionObservationsTable({ conversionObservations }: ConversionObservationsTableProps): ReactNode {
12 | const rows: ReactNode[] = [];
13 | for (const [product, observation] of Object.entries(conversionObservations)) {
14 | rows.push(
15 |
16 | {product}
17 | {formatNumber(observation.bidPrice, 2)}
18 | {formatNumber(observation.askPrice, 2)}
19 | {formatNumber(observation.transportFees, 2)}
20 | {formatNumber(observation.exportTariff, 2)}
21 | {formatNumber(observation.importTariff, 2)}
22 | {formatNumber(observation.sugarPrice, 2)}
23 | {formatNumber(observation.sunlightIndex, 2)}
24 | ,
25 | );
26 | }
27 |
28 | return (
29 |
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/src/components/ErrorAlert.tsx:
--------------------------------------------------------------------------------
1 | import { Alert, AlertProps, Code, List, Text } from '@mantine/core';
2 | import { IconAlertCircle } from '@tabler/icons-react';
3 | import { ReactNode } from 'react';
4 | import { AlgorithmParseError } from '../utils/algorithm.tsx';
5 |
6 | export interface ErrorAlertProps extends Partial {
7 | error: Error;
8 | }
9 |
10 | export function ErrorAlert({ error, ...alertProps }: ErrorAlertProps): ReactNode {
11 | return (
12 | } title="Error" color="red" {...alertProps}>
13 | {error instanceof AlgorithmParseError && (
14 | <>
15 |
16 | Important: before asking for help about this error on Discord or elsewhere, read the prerequisites section
17 | above and double-check the following:
18 |
19 |
20 |
21 | Your code contains the Logger class shown in the prerequisites section above.
22 |
23 |
24 | Your code calls logger.flush() at the end of Trader.run().
25 |
26 |
27 | Your code does not call Python's builtin print() and uses logger.print(){' '}
28 | instead.
29 |
30 |
31 |
32 | When asking for help, make it clear that you have double-checked that your code follows these requirements.
33 |
34 | >
35 | )}
36 | {error instanceof AlgorithmParseError ? error.node : error.message}
37 |
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import '@mantine/core/styles.css';
2 | import '@mantine/dropzone/styles.css';
3 | import '@mantine/code-highlight/styles.css';
4 |
5 | import { createTheme, MantineProvider } from '@mantine/core';
6 | import { ReactNode } from 'react';
7 | import { createBrowserRouter, createRoutesFromElements, Navigate, Route, RouterProvider } from 'react-router-dom';
8 | import { BasePage } from './pages/base/BasePage.tsx';
9 | import { HomePage } from './pages/home/HomePage.tsx';
10 | import { VisualizerPage } from './pages/visualizer/VisualizerPage.tsx';
11 | import { useStore } from './store.ts';
12 |
13 | const theme = createTheme({
14 | colors: {
15 | // Mantine 7.3.0 changes the dark colors to be more slightly lighter than they used to be
16 | // See https://mantine.dev/changelog/7-3-0/#improved-dark-color-scheme-colors for more information
17 | // The old dark colors offer better contrast between default text and background colors
18 | dark: [
19 | '#C1C2C5',
20 | '#A6A7AB',
21 | '#909296',
22 | '#5c5f66',
23 | '#373A40',
24 | '#2C2E33',
25 | '#25262b',
26 | '#1A1B1E',
27 | '#141517',
28 | '#101113',
29 | ],
30 | },
31 | });
32 |
33 | const router = createBrowserRouter(
34 | createRoutesFromElements(
35 | }>
36 | } />
37 | } />
38 | } />
39 | ,
40 | ),
41 | {
42 | basename: '/imc-prosperity-3-visualizer/',
43 | },
44 | );
45 |
46 | export function App(): ReactNode {
47 | const colorScheme = useStore(state => state.colorScheme);
48 |
49 | return (
50 |
51 |
52 |
53 | );
54 | }
55 |
--------------------------------------------------------------------------------
/src/pages/visualizer/ProductPriceChart.tsx:
--------------------------------------------------------------------------------
1 | import Highcharts from 'highcharts';
2 | import { ReactNode } from 'react';
3 | import { ProsperitySymbol } from '../../models.ts';
4 | import { useStore } from '../../store.ts';
5 | import { getAskColor, getBidColor } from '../../utils/colors.ts';
6 | import { Chart } from './Chart.tsx';
7 |
8 | export interface ProductPriceChartProps {
9 | symbol: ProsperitySymbol;
10 | }
11 |
12 | export function ProductPriceChart({ symbol }: ProductPriceChartProps): ReactNode {
13 | const algorithm = useStore(state => state.algorithm)!;
14 |
15 | const series: Highcharts.SeriesOptionsType[] = [
16 | { type: 'line', name: 'Bid 3', color: getBidColor(0.5), marker: { symbol: 'square' }, data: [] },
17 | { type: 'line', name: 'Bid 2', color: getBidColor(0.75), marker: { symbol: 'circle' }, data: [] },
18 | { type: 'line', name: 'Bid 1', color: getBidColor(1.0), marker: { symbol: 'triangle' }, data: [] },
19 | { type: 'line', name: 'Mid price', color: 'gray', dashStyle: 'Dash', marker: { symbol: 'diamond' }, data: [] },
20 | { type: 'line', name: 'Ask 1', color: getAskColor(1.0), marker: { symbol: 'triangle-down' }, data: [] },
21 | { type: 'line', name: 'Ask 2', color: getAskColor(0.75), marker: { symbol: 'circle' }, data: [] },
22 | { type: 'line', name: 'Ask 3', color: getAskColor(0.5), marker: { symbol: 'square' }, data: [] },
23 | ];
24 |
25 | for (const row of algorithm.activityLogs) {
26 | if (row.product !== symbol) {
27 | continue;
28 | }
29 |
30 | for (let i = 0; i < row.bidPrices.length; i++) {
31 | (series[2 - i] as any).data.push([row.timestamp, row.bidPrices[i]]);
32 | }
33 |
34 | (series[3] as any).data.push([row.timestamp, row.midPrice]);
35 |
36 | for (let i = 0; i < row.askPrices.length; i++) {
37 | (series[i + 4] as any).data.push([row.timestamp, row.askPrices[i]]);
38 | }
39 | }
40 |
41 | return ;
42 | }
43 |
--------------------------------------------------------------------------------
/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Single Page Apps for GitHub Pages
6 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | import eslint from '@eslint/js';
2 | import eslintConfigPrettier from 'eslint-config-prettier';
3 | import importPlugin from 'eslint-plugin-import';
4 | import reactPlugin from 'eslint-plugin-react';
5 | import globals from 'globals';
6 | import tseslint from 'typescript-eslint';
7 |
8 | export default tseslint.config(
9 | eslint.configs.recommended,
10 | ...tseslint.configs.recommended,
11 | importPlugin.flatConfigs.errors,
12 | importPlugin.flatConfigs.warnings,
13 | importPlugin.flatConfigs.typescript,
14 | reactPlugin.configs.flat.recommended,
15 | reactPlugin.configs.flat['jsx-runtime'],
16 | eslintConfigPrettier,
17 | {
18 | languageOptions: {
19 | ecmaVersion: 'latest',
20 | sourceType: 'module',
21 | globals: {
22 | ...globals.browser,
23 | ...globals.node,
24 | },
25 | },
26 | ignores: ['*.html'],
27 | rules: {
28 | '@typescript-eslint/no-explicit-any': 'off',
29 | '@typescript-eslint/explicit-member-accessibility': 'error',
30 | '@typescript-eslint/no-inferrable-types': 'off',
31 | '@typescript-eslint/no-var-requires': 'off',
32 | '@typescript-eslint/explicit-function-return-type': [
33 | 'error',
34 | {
35 | allowExpressions: true,
36 | },
37 | ],
38 | '@typescript-eslint/explicit-module-boundary-types': [
39 | 'error',
40 | {
41 | allowArgumentsExplicitlyTypedAsAny: true,
42 | },
43 | ],
44 | '@typescript-eslint/no-unused-vars': [
45 | 'error',
46 | {
47 | caughtErrors: 'none',
48 | },
49 | ],
50 | 'import/order': [
51 | 'error',
52 | {
53 | alphabetize: {
54 | order: 'asc',
55 | caseInsensitive: true,
56 | },
57 | },
58 | ],
59 | 'sort-imports': [
60 | 'error',
61 | {
62 | ignoreCase: true,
63 | ignoreDeclarationSort: true,
64 | },
65 | ],
66 | },
67 | },
68 | );
69 |
--------------------------------------------------------------------------------
/src/pages/base/Header.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Container, Group, Text, Tooltip } from '@mantine/core';
2 | import { IconEye, IconHome } from '@tabler/icons-react';
3 | import { ReactNode } from 'react';
4 | import { Link, useLocation } from 'react-router-dom';
5 | import { useStore } from '../../store.ts';
6 | import classes from './Header.module.css';
7 |
8 | export function Header(): ReactNode {
9 | const location = useLocation();
10 | const algorithm = useStore(state => state.algorithm);
11 |
12 | const links = [
13 |
19 |
20 |
21 |
22 | Home
23 | ,
24 | ];
25 |
26 | if (algorithm !== null) {
27 | links.push(
28 |
34 |
35 |
36 |
37 | Visualizer
38 | ,
39 | );
40 | } else {
41 | links.push(
42 |
43 |
44 |
45 |
46 |
47 | Visualizer
48 |
49 | ,
50 | );
51 | }
52 |
53 | return (
54 |
55 |
56 |
57 |
58 | IMC Prosperity 3 Visualizer
59 |
60 |
61 | {links}
62 |
63 |
64 | );
65 | }
66 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "imc-prosperity-3-visualizer",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "tsc && vite build",
9 | "preview": "vite preview",
10 | "lint": "pnpm run \"/^lint:.*/\"",
11 | "lint:eslint": "eslint --format codeframe '**/*.ts' '**/*.tsx'",
12 | "lint:prettier": "prettier --check '**/*.{ts,tsx,cjs,css,html,json,yml}'",
13 | "fix": "pnpm run --sequential \"/^fix:.*/\"",
14 | "fix:eslint": "pnpm lint:eslint --fix",
15 | "fix:prettier": "prettier --write '**/*.{ts,tsx,cjs,css,html,json,yml}'"
16 | },
17 | "dependencies": {
18 | "@mantine/code-highlight": "^7.17.0",
19 | "@mantine/core": "^7.17.0",
20 | "@mantine/dropzone": "^7.17.0",
21 | "@mantine/hooks": "^7.17.0",
22 | "@tabler/icons": "^3.30.0",
23 | "@tabler/icons-react": "^3.30.0",
24 | "axios": "^1.7.9",
25 | "date-fns": "^4.1.0",
26 | "highcharts": "^11.4.8",
27 | "highcharts-react-official": "^3.2.1",
28 | "lodash": "^4.17.21",
29 | "react": "^19.0.0",
30 | "react-dom": "^19.0.0",
31 | "react-router-dom": "^7.2.0",
32 | "zustand": "^5.0.3"
33 | },
34 | "devDependencies": {
35 | "@eslint/js": "^9.21.0",
36 | "@types/lodash": "^4.17.15",
37 | "@types/react": "^19.0.10",
38 | "@types/react-dom": "^19.0.4",
39 | "@vitejs/plugin-react": "^4.3.4",
40 | "eslint": "^9.21.0",
41 | "eslint-config-prettier": "^10.0.1",
42 | "eslint-formatter-codeframe": "^7.32.1",
43 | "eslint-plugin-import": "^2.31.0",
44 | "eslint-plugin-react": "^7.37.4",
45 | "globals": "^16.0.0",
46 | "postcss": "^8.5.3",
47 | "postcss-preset-mantine": "^1.17.0",
48 | "postcss-simple-vars": "^7.0.1",
49 | "prettier": "^3.5.2",
50 | "typescript": "^5.7.3",
51 | "typescript-eslint": "^8.25.0",
52 | "vite": "^6.1.1"
53 | },
54 | "prettier": {
55 | "printWidth": 120,
56 | "singleQuote": true,
57 | "trailingComma": "all",
58 | "arrowParens": "avoid"
59 | },
60 | "pnpm": {
61 | "onlyBuiltDependencies": [
62 | "esbuild"
63 | ]
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/pages/visualizer/TimestampsCard.tsx:
--------------------------------------------------------------------------------
1 | import { Slider, SliderProps, Text } from '@mantine/core';
2 | import { useHotkeys } from '@mantine/hooks';
3 | import { ReactNode, useState } from 'react';
4 | import { AlgorithmDataRow } from '../../models.ts';
5 | import { useStore } from '../../store.ts';
6 | import { formatNumber } from '../../utils/format.ts';
7 | import { TimestampDetail } from './TimestampDetail.tsx';
8 | import { VisualizerCard } from './VisualizerCard.tsx';
9 |
10 | export function TimestampsCard(): ReactNode {
11 | const algorithm = useStore(state => state.algorithm)!;
12 |
13 | const rowsByTimestamp: Record = {};
14 | for (const row of algorithm.data) {
15 | rowsByTimestamp[row.state.timestamp] = row;
16 | }
17 |
18 | const timestampMin = algorithm.data[0].state.timestamp;
19 | const timestampMax = algorithm.data[algorithm.data.length - 1].state.timestamp;
20 | const timestampStep = algorithm.data[1].state.timestamp - algorithm.data[0].state.timestamp;
21 |
22 | const [timestamp, setTimestamp] = useState(timestampMin);
23 |
24 | const marks: SliderProps['marks'] = [];
25 | for (let i = timestampMin; i < timestampMax; i += (timestampMax + 100) / 4) {
26 | marks.push({
27 | value: i,
28 | label: formatNumber(i),
29 | });
30 | }
31 |
32 | useHotkeys([
33 | ['ArrowLeft', () => setTimestamp(timestamp === timestampMin ? timestamp : timestamp - timestampStep)],
34 | ['ArrowRight', () => setTimestamp(timestamp === timestampMax ? timestamp : timestamp + timestampStep)],
35 | ]);
36 |
37 | return (
38 |
39 | `Timestamp ${formatNumber(value)}`}
45 | value={timestamp}
46 | onChange={setTimestamp}
47 | mb="lg"
48 | />
49 |
50 | {rowsByTimestamp[timestamp] ? (
51 |
52 | ) : (
53 | No logs found for timestamp {formatNumber(timestamp)}
54 | )}
55 |
56 | );
57 | }
58 |
--------------------------------------------------------------------------------
/src/pages/visualizer/PositionChart.tsx:
--------------------------------------------------------------------------------
1 | import Highcharts from 'highcharts';
2 | import { ReactNode } from 'react';
3 | import { Algorithm, ProsperitySymbol } from '../../models.ts';
4 | import { useStore } from '../../store.ts';
5 | import { Chart } from './Chart.tsx';
6 |
7 | function getLimit(algorithm: Algorithm, symbol: ProsperitySymbol): number {
8 | const knownLimits: Record = {
9 | RAINFOREST_RESIN: 50,
10 | KELP: 50,
11 | SQUID_INK: 50,
12 | CROISSANTS: 250,
13 | JAMS: 350,
14 | DJEMBES: 60,
15 | PICNIC_BASKET1: 60,
16 | PICNIC_BASKET2: 100,
17 | VOLCANIC_ROCK: 400,
18 | VOLCANIC_ROCK_VOUCHER_9500: 200,
19 | VOLCANIC_ROCK_VOUCHER_9750: 200,
20 | VOLCANIC_ROCK_VOUCHER_10000: 200,
21 | VOLCANIC_ROCK_VOUCHER_10250: 200,
22 | VOLCANIC_ROCK_VOUCHER_10500: 200,
23 | MAGNIFICENT_MACARONS: 75,
24 | };
25 |
26 | if (knownLimits[symbol] !== undefined) {
27 | return knownLimits[symbol];
28 | }
29 |
30 | // This code will be hit when a new product is added to the competition and the visualizer isn't updated yet
31 | // In that case the visualizer doesn't know the real limit yet, so we make a guess based on the algorithm's positions
32 |
33 | const positions = algorithm.data.map(row => row.state.position[symbol] || 0);
34 | const minPosition = Math.min(...positions);
35 | const maxPosition = Math.max(...positions);
36 |
37 | return Math.max(Math.abs(minPosition), maxPosition);
38 | }
39 |
40 | export interface PositionChartProps {
41 | symbols: string[];
42 | }
43 |
44 | export function PositionChart({ symbols }: PositionChartProps): ReactNode {
45 | const algorithm = useStore(state => state.algorithm)!;
46 |
47 | const limits: Record = {};
48 | for (const symbol of symbols) {
49 | limits[symbol] = getLimit(algorithm, symbol);
50 | }
51 |
52 | const data: Record = {};
53 | for (const symbol of symbols) {
54 | data[symbol] = [];
55 | }
56 |
57 | for (const row of algorithm.data) {
58 | for (const symbol of symbols) {
59 | const position = row.state.position[symbol] || 0;
60 | data[symbol].push([row.state.timestamp, (position / limits[symbol]) * 100]);
61 | }
62 | }
63 |
64 | const series: Highcharts.SeriesOptionsType[] = symbols.map((symbol, i) => ({
65 | type: 'line',
66 | name: symbol,
67 | data: data[symbol],
68 |
69 | // We offset the position color by 1 to make it line up with the colors in the profit / loss chart,
70 | // while keeping the "Total" line in the profit / loss chart the same color at all times
71 | colorIndex: (i + 1) % 10,
72 | }));
73 |
74 | return ;
75 | }
76 |
--------------------------------------------------------------------------------
/src/pages/home/LoadFromUrl.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Code, Text, TextInput } from '@mantine/core';
2 | import axios from 'axios';
3 | import { FormEvent, ReactNode, useCallback, useEffect, useState } from 'react';
4 | import { useNavigate, useSearchParams } from 'react-router-dom';
5 | import { ErrorAlert } from '../../components/ErrorAlert.tsx';
6 | import { useAsync } from '../../hooks/use-async.ts';
7 | import { useStore } from '../../store.ts';
8 | import { parseAlgorithmLogs } from '../../utils/algorithm.tsx';
9 | import { HomeCard } from './HomeCard.tsx';
10 |
11 | export function LoadFromUrl(): ReactNode {
12 | const [url, setUrl] = useState('');
13 |
14 | const algorithm = useStore(state => state.algorithm);
15 | const setAlgorithm = useStore(state => state.setAlgorithm);
16 |
17 | const navigate = useNavigate();
18 | const searchParams = useSearchParams()[0];
19 |
20 | const loadAlgorithm = useAsync(async (logsUrl: string): Promise => {
21 | const logsResponse = await axios.get(logsUrl);
22 | setAlgorithm(parseAlgorithmLogs(logsResponse.data));
23 | navigate(`/visualizer?open=${logsUrl}`);
24 | });
25 |
26 | const onSubmit = useCallback(
27 | (event?: FormEvent) => {
28 | event?.preventDefault();
29 |
30 | if (url.trim().length > 0) {
31 | loadAlgorithm.call(url);
32 | }
33 | },
34 | [loadAlgorithm],
35 | );
36 |
37 | useEffect(() => {
38 | if (algorithm !== null || loadAlgorithm.loading) {
39 | return;
40 | }
41 |
42 | if (!searchParams.has('open')) {
43 | return;
44 | }
45 |
46 | const url = searchParams.get('open') || '';
47 |
48 | setUrl(url);
49 |
50 | if (url.trim().length > 0) {
51 | loadAlgorithm.call(url);
52 | }
53 | }, []);
54 |
55 | const currentUrl = window.location.origin + window.location.pathname;
56 |
57 | return (
58 |
59 |
60 | Supports URLs to log files that are in the same format as the ones generated by the Prosperity servers. This
61 | format is undocumented, but you can get an idea of what it looks like by downloading a log file from a submitted
62 | algorithm. The URL must allow cross-origin requests from the visualizer's website.
63 |
64 |
65 | This input type can also be used by browsing to {currentUrl}?open=<url>.
66 |
67 |
68 | {loadAlgorithm.error && }
69 |
70 |
82 |
83 | );
84 | }
85 |
--------------------------------------------------------------------------------
/src/pages/visualizer/OrderDepthTable.tsx:
--------------------------------------------------------------------------------
1 | import { Table, Text } from '@mantine/core';
2 | import { ReactNode } from 'react';
3 | import { OrderDepth } from '../../models.ts';
4 | import { getAskColor, getBidColor } from '../../utils/colors.ts';
5 | import { formatNumber } from '../../utils/format.ts';
6 | import { OrderDepthTableSpreadRow } from './OrderDepthTableSpreadRow.tsx';
7 |
8 | export interface OrderDepthTableProps {
9 | orderDepth: OrderDepth;
10 | }
11 |
12 | export function OrderDepthTable({ orderDepth }: OrderDepthTableProps): ReactNode {
13 | const rows: ReactNode[] = [];
14 |
15 | const askPrices = Object.keys(orderDepth.sellOrders)
16 | .map(Number)
17 | .sort((a, b) => b - a);
18 | const bidPrices = Object.keys(orderDepth.buyOrders)
19 | .map(Number)
20 | .sort((a, b) => b - a);
21 |
22 | for (let i = 0; i < askPrices.length; i++) {
23 | const price = askPrices[i];
24 |
25 | if (i > 0 && askPrices[i - 1] - price > 1) {
26 | rows.push();
27 | }
28 |
29 | rows.push(
30 |
31 |
32 | {formatNumber(price)}
33 |
34 | {formatNumber(Math.abs(orderDepth.sellOrders[price]))}
35 |
36 | ,
37 | );
38 | }
39 |
40 | if (askPrices.length > 0 && bidPrices.length > 0 && askPrices[askPrices.length - 1] !== bidPrices[0]) {
41 | rows.push();
42 | }
43 |
44 | for (let i = 0; i < bidPrices.length; i++) {
45 | const price = bidPrices[i];
46 |
47 | if (i > 0 && bidPrices[i - 1] - price > 1) {
48 | rows.push();
49 | }
50 |
51 | rows.push(
52 |
53 |
54 | {formatNumber(orderDepth.buyOrders[price])}
55 |
56 | {formatNumber(price)}
57 |
58 | ,
59 | );
60 | }
61 |
62 | if (rows.length === 0) {
63 | return Timestamp has no order depth;
64 | }
65 |
66 | return (
67 |
68 |
69 |
70 | Bid volume
71 | Price
72 | Ask volume
73 |
74 |
75 | {rows}
76 |
77 | );
78 | }
79 |
--------------------------------------------------------------------------------
/src/pages/visualizer/AlgorithmSummaryCard.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Grid, Group, Text, Title } from '@mantine/core';
2 | import { ReactNode } from 'react';
3 | import { ScrollableCodeHighlight } from '../../components/ScrollableCodeHighlight.tsx';
4 | import { useAsync } from '../../hooks/use-async.ts';
5 | import { useStore } from '../../store.ts';
6 | import { downloadAlgorithmLogs, downloadAlgorithmResults } from '../../utils/algorithm.tsx';
7 | import { formatTimestamp } from '../../utils/format.ts';
8 | import { VisualizerCard } from './VisualizerCard.tsx';
9 |
10 | export function AlgorithmSummaryCard(): ReactNode {
11 | const algorithm = useStore(state => state.algorithm)!;
12 | const summary = algorithm.summary!;
13 |
14 | const downloadLogs = useAsync(async () => {
15 | await downloadAlgorithmLogs(summary.id);
16 | });
17 |
18 | const downloadResults = useAsync(async () => {
19 | await downloadAlgorithmResults(summary.id);
20 | });
21 |
22 | return (
23 |
24 |
25 |
26 |
27 |
28 | Id
29 | {summary.id}
30 |
31 |
32 | File name
33 | {summary.fileName}
34 |
35 |
36 | Submitted at
37 | {formatTimestamp(summary.timestamp)}
38 |
39 |
40 | Submitted by
41 |
42 | {summary.user.firstName} {summary.user.lastName}
43 |
44 |
45 |
46 | Status
47 | {summary.status}
48 |
49 |
50 | Round
51 | {summary.round}
52 |
53 |
54 | Selected for round
55 | {summary.selectedForRound ? 'Yes' : 'No'}
56 |
57 |
58 |
59 |
60 | Content
61 |
62 |
63 |
64 |
65 |
68 |
71 |
72 |
73 |
74 |
75 | );
76 | }
77 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by https://www.toptal.com/developers/gitignore/api/node
2 | # Edit at https://www.toptal.com/developers/gitignore?templates=node
3 |
4 | ### Node ###
5 | # Logs
6 | logs
7 | *.log
8 | npm-debug.log*
9 | yarn-debug.log*
10 | yarn-error.log*
11 | lerna-debug.log*
12 | .pnpm-debug.log*
13 |
14 | # Diagnostic reports (https://nodejs.org/api/report.html)
15 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
16 |
17 | # Runtime data
18 | pids
19 | *.pid
20 | *.seed
21 | *.pid.lock
22 |
23 | # Directory for instrumented libs generated by jscoverage/JSCover
24 | lib-cov
25 |
26 | # Coverage directory used by tools like istanbul
27 | coverage
28 | *.lcov
29 |
30 | # nyc test coverage
31 | .nyc_output
32 |
33 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
34 | .grunt
35 |
36 | # Bower dependency directory (https://bower.io/)
37 | bower_components
38 |
39 | # node-waf configuration
40 | .lock-wscript
41 |
42 | # Compiled binary addons (https://nodejs.org/api/addons.html)
43 | build/Release
44 |
45 | # Dependency directories
46 | node_modules/
47 | jspm_packages/
48 |
49 | # Snowpack dependency directory (https://snowpack.dev/)
50 | web_modules/
51 |
52 | # TypeScript cache
53 | *.tsbuildinfo
54 |
55 | # Optional npm cache directory
56 | .npm
57 |
58 | # Optional eslint cache
59 | .eslintcache
60 |
61 | # Optional stylelint cache
62 | .stylelintcache
63 |
64 | # Microbundle cache
65 | .rpt2_cache/
66 | .rts2_cache_cjs/
67 | .rts2_cache_es/
68 | .rts2_cache_umd/
69 |
70 | # Optional REPL history
71 | .node_repl_history
72 |
73 | # Output of 'npm pack'
74 | *.tgz
75 |
76 | # Yarn Integrity file
77 | .yarn-integrity
78 |
79 | # dotenv environment variable files
80 | .env
81 | .env.development.local
82 | .env.test.local
83 | .env.production.local
84 | .env.local
85 |
86 | # parcel-bundler cache (https://parceljs.org/)
87 | .cache
88 | .parcel-cache
89 |
90 | # Next.js build output
91 | .next
92 | out
93 |
94 | # Nuxt.js build / generate output
95 | .nuxt
96 | dist
97 |
98 | # Gatsby files
99 | .cache/
100 | # Comment in the public line in if your project uses Gatsby and not Next.js
101 | # https://nextjs.org/blog/next-9-1#public-directory-support
102 | # public
103 |
104 | # vuepress build output
105 | .vuepress/dist
106 |
107 | # vuepress v2.x temp and cache directory
108 | .temp
109 |
110 | # Docusaurus cache and generated files
111 | .docusaurus
112 |
113 | # Serverless directories
114 | .serverless/
115 |
116 | # FuseBox cache
117 | .fusebox/
118 |
119 | # DynamoDB Local files
120 | .dynamodb/
121 |
122 | # TernJS port file
123 | .tern-port
124 |
125 | # Stores VSCode versions used for testing VSCode extensions
126 | .vscode-test
127 |
128 | # yarn v2
129 | .yarn/cache
130 | .yarn/unplugged
131 | .yarn/build-state.yml
132 | .yarn/install-state.gz
133 | .pnp.*
134 |
135 | ### Node Patch ###
136 | # Serverless Webpack directories
137 | .webpack/
138 |
139 | # Optional stylelint cache
140 |
141 | # SvelteKit build / generate output
142 | .svelte-kit
143 |
144 | # End of https://www.toptal.com/developers/gitignore/api/node
145 |
--------------------------------------------------------------------------------
/src/pages/home/LoadFromFile.tsx:
--------------------------------------------------------------------------------
1 | import { Group, Text } from '@mantine/core';
2 | import { Dropzone, FileRejection } from '@mantine/dropzone';
3 | import { IconUpload } from '@tabler/icons-react';
4 | import { ReactNode, useCallback, useState } from 'react';
5 | import { useNavigate } from 'react-router-dom';
6 | import { ErrorAlert } from '../../components/ErrorAlert.tsx';
7 | import { useAsync } from '../../hooks/use-async.ts';
8 | import { useStore } from '../../store.ts';
9 | import { parseAlgorithmLogs } from '../../utils/algorithm.tsx';
10 | import { HomeCard } from './HomeCard.tsx';
11 |
12 | function DropzoneContent(): ReactNode {
13 | return (
14 |
15 |
16 |
17 | Drag file here or click to select file
18 |
19 |
20 | );
21 | }
22 |
23 | export function LoadFromFile(): ReactNode {
24 | const navigate = useNavigate();
25 |
26 | const [error, setError] = useState();
27 |
28 | const setAlgorithm = useStore(state => state.setAlgorithm);
29 |
30 | const onDrop = useAsync(
31 | (files: File[]) =>
32 | new Promise((resolve, reject) => {
33 | setError(undefined);
34 |
35 | const reader = new FileReader();
36 |
37 | reader.addEventListener('load', () => {
38 | try {
39 | setAlgorithm(parseAlgorithmLogs(reader.result as string));
40 | navigate('/visualizer');
41 | resolve();
42 | } catch (err: any) {
43 | reject(err);
44 | }
45 | });
46 |
47 | reader.addEventListener('error', () => {
48 | reject(new Error('FileReader emitted an error event'));
49 | });
50 |
51 | reader.readAsText(files[0]);
52 | }),
53 | );
54 |
55 | const onReject = useCallback((rejections: FileRejection[]) => {
56 | const messages: string[] = [];
57 |
58 | for (const rejection of rejections) {
59 | const errorType = {
60 | 'file-invalid-type': 'Invalid type, only log files are supported.',
61 | 'file-too-large': 'File too large.',
62 | 'file-too-small': 'File too small.',
63 | 'too-many-files': 'Too many files.',
64 | }[rejection.errors[0].code]!;
65 |
66 | messages.push(`Could not load algorithm from ${rejection.file.name}: ${errorType}`);
67 | }
68 |
69 | setError(new Error(messages.join('
')));
70 | }, []);
71 |
72 | return (
73 |
74 |
75 | Supports log files that are in the same format as the ones generated by the Prosperity servers. This format is
76 | undocumented, but you can get an idea of what it looks like by downloading a log file from a submitted
77 | algorithm.
78 |
79 |
80 | {error && }
81 | {onDrop.error && }
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 | );
93 | }
94 |
--------------------------------------------------------------------------------
/src/models.ts:
--------------------------------------------------------------------------------
1 | export interface UserSummary {
2 | id: number;
3 | firstName: string;
4 | lastName: string;
5 | }
6 |
7 | export interface AlgorithmSummary {
8 | id: string;
9 | content: string;
10 | fileName: string;
11 | round: string;
12 | selectedForRound: boolean;
13 | status: string;
14 | teamId: string;
15 | timestamp: string;
16 | graphLog: string;
17 | user: UserSummary;
18 | }
19 |
20 | export type Time = number;
21 | export type ProsperitySymbol = string;
22 | export type Product = string;
23 | export type Position = number;
24 | export type UserId = string;
25 | export type ObservationValue = number;
26 |
27 | export interface ActivityLogRow {
28 | day: number;
29 | timestamp: number;
30 | product: Product;
31 | bidPrices: number[];
32 | bidVolumes: number[];
33 | askPrices: number[];
34 | askVolumes: number[];
35 | midPrice: number;
36 | profitLoss: number;
37 | }
38 |
39 | export interface Listing {
40 | symbol: ProsperitySymbol;
41 | product: Product;
42 | denomination: Product;
43 | }
44 |
45 | export interface ConversionObservation {
46 | bidPrice: number;
47 | askPrice: number;
48 | transportFees: number;
49 | exportTariff: number;
50 | importTariff: number;
51 | sugarPrice: number;
52 | sunlightIndex: number;
53 | }
54 |
55 | export interface Observation {
56 | plainValueObservations: Record;
57 | conversionObservations: Record;
58 | }
59 |
60 | export interface Order {
61 | symbol: ProsperitySymbol;
62 | price: number;
63 | quantity: number;
64 | }
65 |
66 | export interface OrderDepth {
67 | buyOrders: Record;
68 | sellOrders: Record;
69 | }
70 |
71 | export interface Trade {
72 | symbol: ProsperitySymbol;
73 | price: number;
74 | quantity: number;
75 | buyer: UserId;
76 | seller: UserId;
77 | timestamp: Time;
78 | }
79 |
80 | export interface TradingState {
81 | timestamp: Time;
82 | traderData: string;
83 | listings: Record;
84 | orderDepths: Record;
85 | ownTrades: Record;
86 | marketTrades: Record;
87 | position: Record;
88 | observations: Observation;
89 | }
90 |
91 | export interface AlgorithmDataRow {
92 | state: TradingState;
93 | orders: Record;
94 | conversions: number;
95 | traderData: string;
96 | algorithmLogs: string;
97 | sandboxLogs: string;
98 | }
99 |
100 | export interface Algorithm {
101 | summary?: AlgorithmSummary;
102 | activityLogs: ActivityLogRow[];
103 | data: AlgorithmDataRow[];
104 | }
105 |
106 | export type CompressedListing = [symbol: ProsperitySymbol, product: Product, denomination: Product];
107 |
108 | export type CompressedOrderDepth = [buyOrders: Record, sellOrders: Record];
109 |
110 | export type CompressedTrade = [
111 | symbol: ProsperitySymbol,
112 | price: number,
113 | quantity: number,
114 | buyer: UserId,
115 | seller: UserId,
116 | timestamp: Time,
117 | ];
118 |
119 | export type CompressedConversionObservation = [
120 | bidPrice: number,
121 | askPrice: number,
122 | transportFees: number,
123 | exportTariff: number,
124 | importTariff: number,
125 | sugarPrice: number,
126 | sunlightIndex: number,
127 | ];
128 |
129 | export type CompressedObservations = [
130 | plainValueObservations: Record,
131 | conversionObservations: Record,
132 | ];
133 |
134 | export type CompressedTradingState = [
135 | timestamp: Time,
136 | traderData: string,
137 | listings: CompressedListing[],
138 | orderDepths: Record,
139 | ownTrades: CompressedTrade[],
140 | marketTrades: CompressedTrade[],
141 | position: Record,
142 | observations: CompressedObservations,
143 | ];
144 |
145 | export type CompressedOrder = [symbol: ProsperitySymbol, price: number, quantity: number];
146 |
147 | export type CompressedAlgorithmDataRow = [
148 | state: CompressedTradingState,
149 | orders: CompressedOrder[],
150 | conversions: number,
151 | traderData: string,
152 | logs: string,
153 | ];
154 |
--------------------------------------------------------------------------------
/src/pages/home/AlgorithmDetail.tsx:
--------------------------------------------------------------------------------
1 | import { Accordion, Button, Group, MantineColor, Text } from '@mantine/core';
2 | import axios from 'axios';
3 | import { ReactNode } from 'react';
4 | import { useNavigate } from 'react-router-dom';
5 | import { ErrorAlert } from '../../components/ErrorAlert';
6 | import { ScrollableCodeHighlight } from '../../components/ScrollableCodeHighlight.tsx';
7 | import { useActualColorScheme } from '../../hooks/use-actual-color-scheme.ts';
8 | import { useAsync } from '../../hooks/use-async.ts';
9 | import { AlgorithmSummary } from '../../models.ts';
10 | import { useStore } from '../../store.ts';
11 | import {
12 | downloadAlgorithmLogs,
13 | downloadAlgorithmResults,
14 | getAlgorithmLogsUrl,
15 | parseAlgorithmLogs,
16 | } from '../../utils/algorithm.tsx';
17 | import { formatNumber, formatTimestamp } from '../../utils/format.ts';
18 |
19 | export interface AlgorithmDetailProps {
20 | position: number;
21 | algorithm: AlgorithmSummary;
22 | proxy: string;
23 | }
24 |
25 | export function AlgorithmDetail({ position, algorithm, proxy }: AlgorithmDetailProps): ReactNode {
26 | const setAlgorithm = useStore(state => state.setAlgorithm);
27 |
28 | const navigate = useNavigate();
29 | const colorScheme = useActualColorScheme();
30 |
31 | let statusColor: MantineColor = 'primary';
32 | switch (algorithm.status) {
33 | case 'FINISHED':
34 | statusColor = colorScheme === 'light' ? 'darkgreen' : 'green';
35 | break;
36 | case 'ERROR':
37 | statusColor = 'red';
38 | break;
39 | }
40 |
41 | const downloadLogs = useAsync(async () => {
42 | await downloadAlgorithmLogs(algorithm.id);
43 | });
44 |
45 | const downloadResults = useAsync(async () => {
46 | await downloadAlgorithmResults(algorithm.id);
47 | });
48 |
49 | const openInVisualizer = useAsync(async () => {
50 | const logsUrl = await getAlgorithmLogsUrl(algorithm.id);
51 | const logsResponse = await axios.get(proxy + logsUrl);
52 |
53 | setAlgorithm(parseAlgorithmLogs(logsResponse.data, algorithm));
54 | navigate('/visualizer');
55 | });
56 |
57 | let title = `${algorithm.fileName} • ${formatTimestamp(algorithm.timestamp)}`;
58 |
59 | let profitLoss = 0;
60 | if (algorithm.status === 'FINISHED') {
61 | const graphLogLines = algorithm.graphLog.trim().split('\n');
62 | profitLoss = parseFloat(graphLogLines[graphLogLines.length - 1].split(';')[1]);
63 |
64 | title += ` • FINISHED • PnL ≈ ${formatNumber(profitLoss)}`;
65 | } else {
66 | title += ` • ${algorithm.status}`;
67 | }
68 |
69 | if (algorithm.selectedForRound) {
70 | title += ' • Active';
71 | }
72 |
73 | return (
74 |
75 |
76 |
77 | {position}. {title}
78 |
79 |
80 |
81 | {openInVisualizer.error && }
82 |
83 |
86 |
89 | {algorithm.status === 'FINISHED' && (
90 |
93 | )}
94 |
95 |
96 | Id: {algorithm.id}
97 |
98 |
99 | File name: {algorithm.fileName}
100 |
101 |
102 | Submitted at: {formatTimestamp(algorithm.timestamp)}
103 |
104 |
105 | Submitted by: {algorithm.user.firstName} {algorithm.user.lastName}
106 |
107 |
108 | Status: {algorithm.status}
109 |
110 |
111 | Round: {algorithm.round}
112 |
113 |
114 | Selected for round: {algorithm.selectedForRound ? 'Yes' : 'No'}
115 |
116 | {algorithm.status === 'FINISHED' && (
117 |
118 | Approximate profit / loss: {formatNumber(profitLoss)}
119 |
120 | )}
121 |
122 | Content:
123 |
124 |
125 |
126 |
127 | );
128 | }
129 |
--------------------------------------------------------------------------------
/src/pages/visualizer/VisualizerPage.tsx:
--------------------------------------------------------------------------------
1 | import { Center, Container, Grid, Title } from '@mantine/core';
2 | import { ReactNode } from 'react';
3 | import { Navigate, useLocation } from 'react-router-dom';
4 | import { useStore } from '../../store.ts';
5 | import { formatNumber } from '../../utils/format.ts';
6 | import { AlgorithmSummaryCard } from './AlgorithmSummaryCard.tsx';
7 | import { ConversionPriceChart } from './ConversionPriceChart.tsx';
8 | import { EnvironmentChart } from './EnvironmentChart.tsx';
9 | import { PlainValueObservationChart } from './PlainValueObservationChart.tsx';
10 | import { PositionChart } from './PositionChart.tsx';
11 | import { ProductPriceChart } from './ProductPriceChart.tsx';
12 | import { ProfitLossChart } from './ProfitLossChart.tsx';
13 | import { TimestampsCard } from './TimestampsCard.tsx';
14 | import { TransportChart } from './TransportChart.tsx';
15 | import { VisualizerCard } from './VisualizerCard.tsx';
16 | import { VolumeChart } from './VolumeChart.tsx';
17 |
18 | export function VisualizerPage(): ReactNode {
19 | const algorithm = useStore(state => state.algorithm);
20 |
21 | const { search } = useLocation();
22 |
23 | if (algorithm === null) {
24 | return ;
25 | }
26 |
27 | const conversionProducts = new Set();
28 | for (const row of algorithm.data) {
29 | for (const product of Object.keys(row.state.observations.conversionObservations)) {
30 | conversionProducts.add(product);
31 | }
32 | }
33 |
34 | let profitLoss = 0;
35 | const lastTimestamp = algorithm.activityLogs[algorithm.activityLogs.length - 1].timestamp;
36 | for (let i = algorithm.activityLogs.length - 1; i >= 0 && algorithm.activityLogs[i].timestamp == lastTimestamp; i--) {
37 | profitLoss += algorithm.activityLogs[i].profitLoss;
38 | }
39 |
40 | const symbols = new Set();
41 | const plainValueObservationSymbols = new Set();
42 |
43 | for (let i = 0; i < algorithm.data.length; i += 1000) {
44 | const row = algorithm.data[i];
45 |
46 | for (const key of Object.keys(row.state.listings)) {
47 | symbols.add(key);
48 | }
49 |
50 | for (const key of Object.keys(row.state.observations.plainValueObservations)) {
51 | plainValueObservationSymbols.add(key);
52 | }
53 | }
54 |
55 | const sortedSymbols = [...symbols].sort((a, b) => a.localeCompare(b));
56 | const sortedPlainValueObservationSymbols = [...plainValueObservationSymbols].sort((a, b) => a.localeCompare(b));
57 |
58 | const symbolColumns: ReactNode[] = [];
59 | sortedSymbols.forEach(symbol => {
60 | symbolColumns.push(
61 |
62 |
63 | ,
64 | );
65 |
66 | symbolColumns.push(
67 |
68 |
69 | ,
70 | );
71 |
72 | if (!conversionProducts.has(symbol)) {
73 | return;
74 | }
75 |
76 | symbolColumns.push(
77 |
78 |
79 | ,
80 | );
81 |
82 | symbolColumns.push(
83 |
84 |
85 | ,
86 | );
87 |
88 | symbolColumns.push(
89 |
90 |
91 | ,
92 | );
93 |
94 | symbolColumns.push();
95 | });
96 |
97 | sortedPlainValueObservationSymbols.forEach(symbol => {
98 | symbolColumns.push(
99 |
100 |
101 | ,
102 | );
103 | });
104 |
105 | return (
106 |
107 |
108 |
109 |
110 |
111 | Final Profit / Loss: {formatNumber(profitLoss)}
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 | {symbolColumns}
122 |
123 |
124 |
125 | {algorithm.summary && (
126 |
127 |
128 |
129 | )}
130 |
131 |
132 | );
133 | }
134 |
--------------------------------------------------------------------------------
/src/pages/visualizer/TimestampDetail.tsx:
--------------------------------------------------------------------------------
1 | import { Grid, Text, Title } from '@mantine/core';
2 | import { ReactNode } from 'react';
3 | import { ScrollableCodeHighlight } from '../../components/ScrollableCodeHighlight.tsx';
4 | import { AlgorithmDataRow } from '../../models.ts';
5 | import { useStore } from '../../store.ts';
6 | import { formatNumber } from '../../utils/format.ts';
7 | import { ConversionObservationsTable } from './ConversionObservationsTable.tsx';
8 | import { ListingsTable } from './ListingsTable.tsx';
9 | import { OrderDepthTable } from './OrderDepthTable.tsx';
10 | import { OrdersTable } from './OrdersTable.tsx';
11 | import { PlainValueObservationsTable } from './PlainValueObservationsTable.tsx';
12 | import { PositionTable } from './PositionTable.tsx';
13 | import { ProfitLossTable } from './ProfitLossTable.tsx';
14 | import { TradesTable } from './TradesTable.tsx';
15 |
16 | function formatTraderData(value: any): string {
17 | if (typeof value === 'string') {
18 | return value;
19 | }
20 |
21 | return JSON.stringify(value);
22 | }
23 |
24 | export interface TimestampDetailProps {
25 | row: AlgorithmDataRow;
26 | }
27 |
28 | export function TimestampDetail({
29 | row: { state, orders, conversions, traderData, algorithmLogs, sandboxLogs },
30 | }: TimestampDetailProps): ReactNode {
31 | const algorithm = useStore(state => state.algorithm)!;
32 |
33 | const profitLoss = algorithm.activityLogs
34 | .filter(row => row.timestamp === state.timestamp)
35 | .reduce((acc, val) => acc + val.profitLoss, 0);
36 |
37 | return (
38 |
39 |
40 | {/* prettier-ignore */}
41 |
42 | Timestamp {formatNumber(state.timestamp)} • Profit / Loss: {formatNumber(profitLoss)} •
43 | Conversions: {formatNumber(conversions)}
44 |
45 |
46 |
47 | Listings
48 |
49 |
50 |
51 | Positions
52 |
53 |
54 |
55 | Profit / Loss
56 |
57 |
58 | {Object.entries(state.orderDepths).map(([symbol, orderDepth], i) => (
59 |
60 | {symbol} order depth
61 |
62 |
63 | ))}
64 | {Object.keys(state.orderDepths).length % 3 <= 2 && }
65 | {Object.keys(state.orderDepths).length % 3 <= 1 && }
66 |
67 | Own trades
68 | {}
69 |
70 |
71 | Market trades
72 | {}
73 |
74 |
75 | Orders
76 | {}
77 |
78 |
79 | Plain value observations
80 |
81 |
82 |
83 | Conversion observations
84 |
85 |
86 |
87 | Sandbox logs
88 | {sandboxLogs ? (
89 |
90 | ) : (
91 | Timestamp has no sandbox logs
92 | )}
93 |
94 |
95 | Algorithm logs
96 | {algorithmLogs ? (
97 |
98 | ) : (
99 | Timestamp has no algorithm logs
100 | )}
101 |
102 |
103 | Previous trader data
104 | {state.traderData ? (
105 |
106 | ) : (
107 | Timestamp has no previous trader data
108 | )}
109 |
110 |
111 | Next trader data
112 | {traderData ? (
113 |
114 | ) : (
115 | Timestamp has no next trader data
116 | )}
117 |
118 |
119 | );
120 | }
121 |
--------------------------------------------------------------------------------
/src/pages/visualizer/Chart.tsx:
--------------------------------------------------------------------------------
1 | import Highcharts from 'highcharts/highstock';
2 | import HighchartsAccessibility from 'highcharts/modules/accessibility';
3 | import HighchartsExporting from 'highcharts/modules/exporting';
4 | import HighchartsOfflineExporting from 'highcharts/modules/offline-exporting';
5 | import HighchartsHighContrastDarkTheme from 'highcharts/themes/high-contrast-dark';
6 | import HighchartsReact from 'highcharts-react-official';
7 | import merge from 'lodash/merge';
8 | import { ReactNode, useMemo } from 'react';
9 | import { useActualColorScheme } from '../../hooks/use-actual-color-scheme.ts';
10 | import { formatNumber } from '../../utils/format.ts';
11 | import { VisualizerCard } from './VisualizerCard.tsx';
12 |
13 | HighchartsAccessibility(Highcharts);
14 | HighchartsExporting(Highcharts);
15 | HighchartsOfflineExporting(Highcharts);
16 |
17 | // Highcharts themes are distributed as Highcharts extensions
18 | // The normal way to use them is to apply these extensions to the global Highcharts object
19 | // However, themes work by overriding the default options, with no way to rollback
20 | // To make theme switching work, we merge theme options into the local chart options instead
21 | // This way we don't override the global defaults and can change themes without refreshing
22 | // This function is a little workaround to be able to get the options a theme overrides
23 | function getThemeOptions(theme: (highcharts: typeof Highcharts) => void): Highcharts.Options {
24 | const highchartsMock = {
25 | _modules: {
26 | 'Core/Globals.js': {
27 | theme: null,
28 | },
29 | 'Core/Defaults.js': {
30 | setOptions: () => {
31 | // Do nothing
32 | },
33 | },
34 | },
35 | win: {
36 | dispatchEvent: () => {},
37 | },
38 | };
39 |
40 | theme(highchartsMock as any);
41 |
42 | return highchartsMock._modules['Core/Globals.js'].theme! as Highcharts.Options;
43 | }
44 |
45 | interface ChartProps {
46 | title: string;
47 | options?: Highcharts.Options;
48 | series: Highcharts.SeriesOptionsType[];
49 | min?: number;
50 | max?: number;
51 | }
52 |
53 | export function Chart({ title, options, series, min, max }: ChartProps): ReactNode {
54 | const colorScheme = useActualColorScheme();
55 |
56 | const fullOptions = useMemo((): Highcharts.Options => {
57 | const themeOptions = colorScheme === 'light' ? {} : getThemeOptions(HighchartsHighContrastDarkTheme);
58 |
59 | const chartOptions: Highcharts.Options = {
60 | chart: {
61 | animation: false,
62 | height: 400,
63 | zooming: {
64 | type: 'x',
65 | },
66 | panning: {
67 | enabled: true,
68 | type: 'x',
69 | },
70 | panKey: 'shift',
71 | numberFormatter: formatNumber,
72 | events: {
73 | load() {
74 | Highcharts.addEvent(this.tooltip, 'headerFormatter', (e: any) => {
75 | if (e.isFooter) {
76 | return true;
77 | }
78 |
79 | let timestamp = e.labelConfig.point.x;
80 |
81 | if (e.labelConfig.point.dataGroup) {
82 | const xData = e.labelConfig.series.xData;
83 | const lastTimestamp = xData[xData.length - 1];
84 | if (timestamp + 100 * e.labelConfig.point.dataGroup.length >= lastTimestamp) {
85 | timestamp = lastTimestamp;
86 | }
87 | }
88 |
89 | e.text = `Timestamp ${formatNumber(timestamp)}
`;
90 | return false;
91 | });
92 | },
93 | },
94 | },
95 | title: {
96 | text: title,
97 | },
98 | credits: {
99 | href: 'javascript:window.open("https://www.highcharts.com/?credits", "_blank")',
100 | },
101 | plotOptions: {
102 | series: {
103 | dataGrouping: {
104 | approximation(this: any, values: number[]): number {
105 | const endIndex = this.dataGroupInfo.start + this.dataGroupInfo.length;
106 | if (endIndex < this.xData.length) {
107 | return values[0];
108 | } else {
109 | return values[values.length - 1];
110 | }
111 | },
112 | anchor: 'start',
113 | firstAnchor: 'firstPoint',
114 | lastAnchor: 'lastPoint',
115 | units: [['second', [1, 2, 5, 10]]],
116 | },
117 | },
118 | },
119 | xAxis: {
120 | type: 'datetime',
121 | title: {
122 | text: 'Timestamp',
123 | },
124 | crosshair: {
125 | width: 1,
126 | },
127 | labels: {
128 | formatter: params => formatNumber(params.value as number),
129 | },
130 | },
131 | yAxis: {
132 | opposite: false,
133 | allowDecimals: false,
134 | min,
135 | max,
136 | },
137 | tooltip: {
138 | split: false,
139 | shared: true,
140 | outside: true,
141 | },
142 | legend: {
143 | enabled: true,
144 | },
145 | rangeSelector: {
146 | enabled: false,
147 | },
148 | navigator: {
149 | enabled: false,
150 | },
151 | scrollbar: {
152 | enabled: false,
153 | },
154 | series,
155 | ...options,
156 | };
157 |
158 | return merge(themeOptions, chartOptions);
159 | }, [colorScheme, title, options, series, min, max]);
160 |
161 | return (
162 |
163 |
164 |
165 | );
166 | }
167 |
--------------------------------------------------------------------------------
/src/pages/home/HomePage.tsx:
--------------------------------------------------------------------------------
1 | import { Anchor, Code, Container, Stack, Text } from '@mantine/core';
2 | import { ReactNode } from 'react';
3 | import { ScrollableCodeHighlight } from '../../components/ScrollableCodeHighlight.tsx';
4 | import { HomeCard } from './HomeCard.tsx';
5 | import { LoadFromFile } from './LoadFromFile.tsx';
6 | import { LoadFromProsperity } from './LoadFromProsperity.tsx';
7 | import { LoadFromUrl } from './LoadFromUrl.tsx';
8 |
9 | export function HomePage(): ReactNode {
10 | const exampleCode = `
11 | import json
12 | from typing import Any
13 |
14 | from datamodel import Listing, Observation, Order, OrderDepth, ProsperityEncoder, Symbol, Trade, TradingState
15 |
16 |
17 | class Logger:
18 | def __init__(self) -> None:
19 | self.logs = ""
20 | self.max_log_length = 3750
21 |
22 | def print(self, *objects: Any, sep: str = " ", end: str = "\\n") -> None:
23 | self.logs += sep.join(map(str, objects)) + end
24 |
25 | def flush(self, state: TradingState, orders: dict[Symbol, list[Order]], conversions: int, trader_data: str) -> None:
26 | base_length = len(
27 | self.to_json(
28 | [
29 | self.compress_state(state, ""),
30 | self.compress_orders(orders),
31 | conversions,
32 | "",
33 | "",
34 | ]
35 | )
36 | )
37 |
38 | # We truncate state.traderData, trader_data, and self.logs to the same max. length to fit the log limit
39 | max_item_length = (self.max_log_length - base_length) // 3
40 |
41 | print(
42 | self.to_json(
43 | [
44 | self.compress_state(state, self.truncate(state.traderData, max_item_length)),
45 | self.compress_orders(orders),
46 | conversions,
47 | self.truncate(trader_data, max_item_length),
48 | self.truncate(self.logs, max_item_length),
49 | ]
50 | )
51 | )
52 |
53 | self.logs = ""
54 |
55 | def compress_state(self, state: TradingState, trader_data: str) -> list[Any]:
56 | return [
57 | state.timestamp,
58 | trader_data,
59 | self.compress_listings(state.listings),
60 | self.compress_order_depths(state.order_depths),
61 | self.compress_trades(state.own_trades),
62 | self.compress_trades(state.market_trades),
63 | state.position,
64 | self.compress_observations(state.observations),
65 | ]
66 |
67 | def compress_listings(self, listings: dict[Symbol, Listing]) -> list[list[Any]]:
68 | compressed = []
69 | for listing in listings.values():
70 | compressed.append([listing.symbol, listing.product, listing.denomination])
71 |
72 | return compressed
73 |
74 | def compress_order_depths(self, order_depths: dict[Symbol, OrderDepth]) -> dict[Symbol, list[Any]]:
75 | compressed = {}
76 | for symbol, order_depth in order_depths.items():
77 | compressed[symbol] = [order_depth.buy_orders, order_depth.sell_orders]
78 |
79 | return compressed
80 |
81 | def compress_trades(self, trades: dict[Symbol, list[Trade]]) -> list[list[Any]]:
82 | compressed = []
83 | for arr in trades.values():
84 | for trade in arr:
85 | compressed.append(
86 | [
87 | trade.symbol,
88 | trade.price,
89 | trade.quantity,
90 | trade.buyer,
91 | trade.seller,
92 | trade.timestamp,
93 | ]
94 | )
95 |
96 | return compressed
97 |
98 | def compress_observations(self, observations: Observation) -> list[Any]:
99 | conversion_observations = {}
100 | for product, observation in observations.conversionObservations.items():
101 | conversion_observations[product] = [
102 | observation.bidPrice,
103 | observation.askPrice,
104 | observation.transportFees,
105 | observation.exportTariff,
106 | observation.importTariff,
107 | observation.sugarPrice,
108 | observation.sunlightIndex,
109 | ]
110 |
111 | return [observations.plainValueObservations, conversion_observations]
112 |
113 | def compress_orders(self, orders: dict[Symbol, list[Order]]) -> list[list[Any]]:
114 | compressed = []
115 | for arr in orders.values():
116 | for order in arr:
117 | compressed.append([order.symbol, order.price, order.quantity])
118 |
119 | return compressed
120 |
121 | def to_json(self, value: Any) -> str:
122 | return json.dumps(value, cls=ProsperityEncoder, separators=(",", ":"))
123 |
124 | def truncate(self, value: str, max_length: int) -> str:
125 | lo, hi = 0, min(len(value), max_length)
126 | out = ""
127 |
128 | while lo <= hi:
129 | mid = (lo + hi) // 2
130 |
131 | candidate = value[:mid]
132 | if len(candidate) < len(value):
133 | candidate += "..."
134 |
135 | encoded_candidate = json.dumps(candidate)
136 |
137 | if len(encoded_candidate) <= max_length:
138 | out = candidate
139 | lo = mid + 1
140 | else:
141 | hi = mid - 1
142 |
143 | return out
144 |
145 |
146 | logger = Logger()
147 |
148 |
149 | class Trader:
150 | def run(self, state: TradingState) -> tuple[dict[Symbol, list[Order]], int, str]:
151 | result = {}
152 | conversions = 0
153 | trader_data = ""
154 |
155 | # TODO: Add logic
156 |
157 | logger.flush(state, result, conversions, trader_data)
158 | return result, conversions, trader_data
159 | `.trim();
160 |
161 | return (
162 |
163 |
164 |
165 | {/* prettier-ignore */}
166 |
167 | IMC Prosperity 3 Visualizer is a visualizer for IMC Prosperity 3 algorithms.
168 | Its source code is available in the jmerle/imc-prosperity-3-visualizer GitHub repository.
169 | Load an algorithm below to get started.
170 |
171 |
172 |
173 |
174 |
175 | IMC Prosperity 3 Visualizer assumes your algorithm logs in a certain format. Algorithms that use a different
176 | logging format may cause unexpected errors when opening them in the visualizer. Please use the following
177 | boilerplate for your algorithm (or adapt your algorithm to use the logger from this code) and use{' '}
178 | logger.print() where you would normally use print():
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 | );
189 | }
190 |
--------------------------------------------------------------------------------
/src/pages/home/LoadFromProsperity.tsx:
--------------------------------------------------------------------------------
1 | import { Anchor, Button, Code, Kbd, PasswordInput, Select, Text, TextInput } from '@mantine/core';
2 | import { AxiosResponse } from 'axios';
3 | import { FormEvent, ReactNode, useCallback, useState } from 'react';
4 | import { ErrorAlert } from '../../components/ErrorAlert.tsx';
5 | import { useAsync } from '../../hooks/use-async.ts';
6 | import { AlgorithmSummary } from '../../models.ts';
7 | import { useStore } from '../../store.ts';
8 | import { authenticatedAxios } from '../../utils/axios.ts';
9 | import { formatTimestamp } from '../../utils/format.ts';
10 | import { AlgorithmList } from './AlgorithmList.tsx';
11 | import { HomeCard } from './HomeCard.tsx';
12 |
13 | export function LoadFromProsperity(): ReactNode {
14 | /*
15 | Generated by https://chriszarate.github.io/bookmarkleter/
16 | Raw code:
17 | if (window.location.hostname !== 'prosperity.imc.com') {
18 | alert('This bookmarklet should only be used on prosperity.imc.com');
19 | } else {
20 | const key = Object.keys(window.localStorage).find(key => key.endsWith('.idToken'));
21 | if (key !== undefined) {
22 | navigator.clipboard.writeText(window.localStorage.getItem(key));
23 | alert('Successfully copied ID token to clipboard!');
24 | } else {
25 | alert('ID token not found, are you sure you are logged in?');
26 | }
27 | }
28 | */
29 | const bookmarklet =
30 | 'javascript:void%20function(){if(%22prosperity.imc.com%22!==window.location.hostname)alert(%22This%20bookmarklet%20should%20only%20be%20used%20on%20prosperity.imc.com%22);else{const%20a=Object.keys(window.localStorage).find(a=%3Ea.endsWith(%22.idToken%22));a===void%200%3Falert(%22ID%20token%20not%20found,%20are%20you%20sure%20you%20are%20logged%20in%3F%22):(navigator.clipboard.writeText(window.localStorage.getItem(a)),alert(%22Successfully%20copied%20ID%20token%20to%20clipboard!%22))}}();';
31 |
32 | // React shows an error when using "javascript:" URLs without dangerouslySetInnerHTML
33 | const bookmarkletHtml = `IMC Prosperity ID Token Retriever`;
34 |
35 | const idToken = useStore(state => state.idToken);
36 | const setIdToken = useStore(state => state.setIdToken);
37 |
38 | const round = useStore(state => state.round);
39 | const setRound = useStore(state => state.setRound);
40 |
41 | const [proxy, setProxy] = useState('https://imc-prosperity-3-visualizer-cors-anywhere.jmerle.dev/');
42 |
43 | const loadAlgorithms = useAsync(async (): Promise => {
44 | let response: AxiosResponse;
45 | try {
46 | response = await authenticatedAxios.get(
47 | `https://bz97lt8b1e.execute-api.eu-west-1.amazonaws.com/prod/submission/algo/${round}`,
48 | );
49 | } catch (err: any) {
50 | if (err.response?.status === 403) {
51 | throw new Error('ID token is invalid, please change it.');
52 | }
53 |
54 | throw err;
55 | }
56 |
57 | return response.data.sort((a, b) => Date.parse(b.timestamp) - Date.parse(a.timestamp));
58 | });
59 |
60 | const onSubmit = useCallback(
61 | (event?: FormEvent) => {
62 | event?.preventDefault();
63 |
64 | if (idToken.trim().length > 0) {
65 | loadAlgorithms.call();
66 | }
67 | },
68 | [loadAlgorithms],
69 | );
70 |
71 | const now = Date.now();
72 | const rounds = [
73 | { value: 'ROUND0', label: 'Tutorial', openFrom: '2025-02-24T11:00:00.000Z' },
74 | { value: 'ROUND1', label: 'Round 1', openFrom: '2025-04-07T11:00:00.000Z' },
75 | { value: 'ROUND2', label: 'Round 2', openFrom: '2025-04-10T11:00:00.000Z' },
76 | { value: 'ROUND3', label: 'Round 3', openFrom: '2025-04-13T11:00:00.000Z' },
77 | { value: 'ROUND4', label: 'Round 4', openFrom: '2025-04-16T11:00:00.000Z' },
78 | { value: 'ROUND5', label: 'Round 5', openFrom: '2025-04-19T11:00:00.000Z' },
79 | ].map(round => {
80 | const disabled = Date.parse(round.openFrom) > now;
81 | const label = disabled ? `${round.label} - Available from ${formatTimestamp(round.openFrom)}` : round.label;
82 |
83 | return {
84 | value: round.value,
85 | label,
86 | disabled,
87 | };
88 | });
89 |
90 | return (
91 |
92 |
93 | Requires your Prosperity ID token that is stored in the local storage item with the
94 | CognitoIdentityServiceProvider.<some id>.<email>.idToken key on the Prosperity website.
95 | You can inspect the local storage items of a website by having the website open in the active tab, pressing{' '}
96 | F12 to open the browser's developer tools, and going to the Application (Chrome) or{' '}
97 | Storage (Firefox) tab. From there, click on Local Storage in the sidebar and select the website
98 | that appears underneath the sidebar entry.
99 |
100 |
101 | To make extracting the ID token easier, you can drag the following bookmarklet into your browser's
102 | bookmarks bar. When you click it on the Prosperity website, the ID token is copied to the clipboard
103 | automatically.
104 |
105 |
106 |
107 |
108 | The visualizer remembers the ID token for ease-of-use, but the token is only valid for a limited amount of time
109 | so you'll need to update this field often. Your ID token is only used to list your algorithms and to
110 | download algorithm logs and results. The visualizer communicates directly with the API used by the Prosperity
111 | website and never sends data to other servers.
112 |
113 | {/* prettier-ignore */}
114 |
115 | By default the "Open in visualizer" button routes the HTTP request to download the algorithm's logs through a CORS Anywhere instance hosted by the creator of this visualizer.
116 | This is necessary because the logs need to be downloaded from an AWS S3 endpoint without Access-Control-Allow-Origin headers that allow downloads from this visualizer.
117 | While I promise no log data is persisted server-side, you are free to change the proxy to one hosted by yourself.
118 |
119 |
120 | {loadAlgorithms.error && }
121 |
122 |
151 |
152 | {loadAlgorithms.success && }
153 |
154 | );
155 | }
156 |
--------------------------------------------------------------------------------
/src/utils/algorithm.tsx:
--------------------------------------------------------------------------------
1 | import { Text } from '@mantine/core';
2 | import { ReactNode } from 'react';
3 | import {
4 | ActivityLogRow,
5 | Algorithm,
6 | AlgorithmDataRow,
7 | AlgorithmSummary,
8 | CompressedAlgorithmDataRow,
9 | CompressedListing,
10 | CompressedObservations,
11 | CompressedOrder,
12 | CompressedOrderDepth,
13 | CompressedTrade,
14 | CompressedTradingState,
15 | ConversionObservation,
16 | Listing,
17 | Observation,
18 | Order,
19 | OrderDepth,
20 | Product,
21 | ProsperitySymbol,
22 | Trade,
23 | TradingState,
24 | } from '../models.ts';
25 | import { authenticatedAxios } from './axios.ts';
26 |
27 | export class AlgorithmParseError extends Error {
28 | public constructor(public readonly node: ReactNode) {
29 | super('Failed to parse algorithm logs');
30 | }
31 | }
32 |
33 | function getColumnValues(columns: string[], indices: number[]): number[] {
34 | const values: number[] = [];
35 |
36 | for (const index of indices) {
37 | const value = columns[index];
38 | if (value !== '') {
39 | values.push(parseFloat(value));
40 | }
41 | }
42 |
43 | return values;
44 | }
45 |
46 | function getActivityLogs(logLines: string[]): ActivityLogRow[] {
47 | const headerIndex = logLines.indexOf('Activities log:');
48 | if (headerIndex === -1) {
49 | return [];
50 | }
51 |
52 | const rows: ActivityLogRow[] = [];
53 |
54 | for (let i = headerIndex + 2; i < logLines.length; i++) {
55 | const line = logLines[i];
56 | if (line === '') {
57 | break;
58 | }
59 |
60 | const columns = line.split(';');
61 |
62 | rows.push({
63 | day: Number(columns[0]),
64 | timestamp: Number(columns[1]),
65 | product: columns[2],
66 | bidPrices: getColumnValues(columns, [3, 5, 7]),
67 | bidVolumes: getColumnValues(columns, [4, 6, 8]),
68 | askPrices: getColumnValues(columns, [9, 11, 13]),
69 | askVolumes: getColumnValues(columns, [10, 12, 14]),
70 | midPrice: Number(columns[15]),
71 | profitLoss: Number(columns[16]),
72 | });
73 | }
74 |
75 | return rows;
76 | }
77 |
78 | function decompressListings(compressed: CompressedListing[]): Record {
79 | const listings: Record = {};
80 |
81 | for (const [symbol, product, denomination] of compressed) {
82 | listings[symbol] = {
83 | symbol,
84 | product,
85 | denomination,
86 | };
87 | }
88 |
89 | return listings;
90 | }
91 |
92 | function decompressOrderDepths(
93 | compressed: Record,
94 | ): Record {
95 | const orderDepths: Record = {};
96 |
97 | for (const [symbol, [buyOrders, sellOrders]] of Object.entries(compressed)) {
98 | orderDepths[symbol] = {
99 | buyOrders,
100 | sellOrders,
101 | };
102 | }
103 |
104 | return orderDepths;
105 | }
106 |
107 | function decompressTrades(compressed: CompressedTrade[]): Record {
108 | const trades: Record = {};
109 |
110 | for (const [symbol, price, quantity, buyer, seller, timestamp] of compressed) {
111 | if (trades[symbol] === undefined) {
112 | trades[symbol] = [];
113 | }
114 |
115 | trades[symbol].push({
116 | symbol,
117 | price,
118 | quantity,
119 | buyer,
120 | seller,
121 | timestamp,
122 | });
123 | }
124 |
125 | return trades;
126 | }
127 |
128 | function decompressObservations(compressed: CompressedObservations): Observation {
129 | const conversionObservations: Record = {};
130 |
131 | for (const [
132 | product,
133 | [bidPrice, askPrice, transportFees, exportTariff, importTariff, sugarPrice, sunlightIndex],
134 | ] of Object.entries(compressed[1])) {
135 | conversionObservations[product] = {
136 | bidPrice,
137 | askPrice,
138 | transportFees,
139 | exportTariff,
140 | importTariff,
141 | sugarPrice,
142 | sunlightIndex,
143 | };
144 | }
145 |
146 | return {
147 | plainValueObservations: compressed[0],
148 | conversionObservations,
149 | };
150 | }
151 |
152 | function decompressState(compressed: CompressedTradingState): TradingState {
153 | return {
154 | timestamp: compressed[0],
155 | traderData: compressed[1],
156 | listings: decompressListings(compressed[2]),
157 | orderDepths: decompressOrderDepths(compressed[3]),
158 | ownTrades: decompressTrades(compressed[4]),
159 | marketTrades: decompressTrades(compressed[5]),
160 | position: compressed[6],
161 | observations: decompressObservations(compressed[7]),
162 | };
163 | }
164 |
165 | function decompressOrders(compressed: CompressedOrder[]): Record {
166 | const orders: Record = {};
167 |
168 | for (const [symbol, price, quantity] of compressed) {
169 | if (orders[symbol] === undefined) {
170 | orders[symbol] = [];
171 | }
172 |
173 | orders[symbol].push({
174 | symbol,
175 | price,
176 | quantity,
177 | });
178 | }
179 |
180 | return orders;
181 | }
182 |
183 | function decompressDataRow(compressed: CompressedAlgorithmDataRow, sandboxLogs: string): AlgorithmDataRow {
184 | return {
185 | state: decompressState(compressed[0]),
186 | orders: decompressOrders(compressed[1]),
187 | conversions: compressed[2],
188 | traderData: compressed[3],
189 | algorithmLogs: compressed[4],
190 | sandboxLogs,
191 | };
192 | }
193 |
194 | function getAlgorithmData(logLines: string[]): AlgorithmDataRow[] {
195 | const headerIndex = logLines.indexOf('Sandbox logs:');
196 | if (headerIndex === -1) {
197 | return [];
198 | }
199 |
200 | const rows: AlgorithmDataRow[] = [];
201 | let nextSandboxLogs = '';
202 |
203 | const sandboxLogPrefix = ' "sandboxLog": ';
204 | const lambdaLogPrefix = ' "lambdaLog": ';
205 |
206 | for (let i = headerIndex + 1; i < logLines.length; i++) {
207 | const line = logLines[i];
208 | if (line.endsWith(':')) {
209 | break;
210 | }
211 |
212 | if (line.startsWith(sandboxLogPrefix)) {
213 | nextSandboxLogs = JSON.parse(line.substring(sandboxLogPrefix.length, line.length - 1)).trim();
214 |
215 | if (nextSandboxLogs.startsWith('Conversion request')) {
216 | const lastRow = rows[rows.length - 1];
217 | lastRow.sandboxLogs += (lastRow.sandboxLogs.length > 0 ? '\n' : '') + nextSandboxLogs;
218 |
219 | nextSandboxLogs = '';
220 | }
221 |
222 | continue;
223 | }
224 |
225 | if (!line.startsWith(lambdaLogPrefix) || line === ' "lambdaLog": "",') {
226 | continue;
227 | }
228 |
229 | const start = line.indexOf('[[');
230 | const end = line.lastIndexOf(']') + 1;
231 |
232 | try {
233 | const compressedDataRow = JSON.parse(JSON.parse('"' + line.substring(start, end) + '"'));
234 | rows.push(decompressDataRow(compressedDataRow, nextSandboxLogs));
235 | } catch (err) {
236 | console.log(line);
237 | console.error(err);
238 |
239 | throw new AlgorithmParseError(
240 | (
241 | <>
242 | Logs are in invalid format. Could not parse the following line:
243 | {line}
244 | >
245 | ),
246 | );
247 | }
248 | }
249 |
250 | return rows;
251 | }
252 |
253 | export function parseAlgorithmLogs(logs: string, summary?: AlgorithmSummary): Algorithm {
254 | const logLines = logs.trim().split(/\r?\n/);
255 |
256 | const activityLogs = getActivityLogs(logLines);
257 | const data = getAlgorithmData(logLines);
258 |
259 | if (activityLogs.length === 0 && data.length === 0) {
260 | throw new AlgorithmParseError(
261 | (
262 |
263 | Logs are empty, either something went wrong with your submission or your backtester logs in a different format
264 | than Prosperity's submission environment.
265 |
266 | ),
267 | );
268 | }
269 |
270 | if (activityLogs.length === 0 || data.length === 0) {
271 | throw new AlgorithmParseError(
272 | /* prettier-ignore */
273 | Logs are in invalid format.,
274 | );
275 | }
276 |
277 | return {
278 | summary,
279 | activityLogs,
280 | data,
281 | };
282 | }
283 |
284 | export async function getAlgorithmLogsUrl(algorithmId: string): Promise {
285 | const urlResponse = await authenticatedAxios.get(
286 | `https://bz97lt8b1e.execute-api.eu-west-1.amazonaws.com/prod/submission/logs/${algorithmId}`,
287 | );
288 |
289 | return urlResponse.data;
290 | }
291 |
292 | function downloadFile(url: string): void {
293 | const link = document.createElement('a');
294 | link.href = url;
295 | link.download = new URL(url).pathname.split('/').pop()!;
296 | link.target = '_blank';
297 | link.rel = 'noreferrer';
298 |
299 | document.body.appendChild(link);
300 | link.click();
301 | link.remove();
302 | }
303 |
304 | export async function downloadAlgorithmLogs(algorithmId: string): Promise {
305 | const logsUrl = await getAlgorithmLogsUrl(algorithmId);
306 | downloadFile(logsUrl);
307 | }
308 |
309 | export async function downloadAlgorithmResults(algorithmId: string): Promise {
310 | const detailsResponse = await authenticatedAxios.get(
311 | `https://bz97lt8b1e.execute-api.eu-west-1.amazonaws.com/prod/results/tutorial/${algorithmId}`,
312 | );
313 |
314 | downloadFile(detailsResponse.data.algo.summary.activitiesLog);
315 | }
316 |
--------------------------------------------------------------------------------