├── 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 | [![Build Status](https://github.com/jmerle/imc-prosperity-3-visualizer/workflows/Build/badge.svg)](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 |
71 | setUrl((e.target as HTMLInputElement).value)} 76 | /> 77 | 78 | 81 | 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 |
123 | setIdToken((e.target as HTMLInputElement).value)} 128 | /> 129 | 130 |