├── src ├── vite-env.d.ts ├── main.tsx ├── utils │ ├── colors.ts │ ├── format.ts │ ├── axios.ts │ ├── async.ts │ └── algorithm.ts ├── pages │ ├── base │ │ ├── ScrollToTop.tsx │ │ ├── PrismScrollArea.tsx │ │ ├── ThemeSwitch.tsx │ │ └── Page.tsx │ ├── visualizer │ │ ├── OrderDepthTableSpreadRow.tsx │ │ ├── VisualizerCard.tsx │ │ ├── SimpleTable.tsx │ │ ├── ObservationChart.tsx │ │ ├── ListingTable.tsx │ │ ├── SubmissionLogsCard.tsx │ │ ├── PositionTable.tsx │ │ ├── OrderTable.tsx │ │ ├── ProfitLossTable.tsx │ │ ├── TradeTable.tsx │ │ ├── ProfitLossChart.tsx │ │ ├── VolumeChart.tsx │ │ ├── PriceChart.tsx │ │ ├── SandboxLogsCard.tsx │ │ ├── PositionChart.tsx │ │ ├── OrderDepthTable.tsx │ │ ├── AlgorithmSummaryCard.tsx │ │ ├── SandboxLogDetail.tsx │ │ ├── VisualizerPage.tsx │ │ └── Chart.tsx │ └── home │ │ ├── HomeCard.tsx │ │ ├── ErrorAlert.tsx │ │ ├── AlgorithmList.tsx │ │ ├── LoadFromElsewhere.tsx │ │ ├── LoadFromFile.tsx │ │ ├── LoadFromProsperity.tsx │ │ ├── AlgorithmDetail.tsx │ │ └── HomePage.tsx ├── store.tsx ├── App.tsx └── models.ts ├── .editorconfig ├── tsconfig.node.json ├── vite.config.ts ├── .gitignore ├── tsconfig.json ├── README.md ├── .github └── workflows │ └── build.yml ├── LICENSE ├── index.html ├── 404.html └── package.json /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom/client'; 2 | import { App } from './App'; 3 | 4 | ReactDOM.createRoot(document.getElementById('root')!).render(); 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 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-visualizer/', 8 | }); 9 | -------------------------------------------------------------------------------- /src/utils/format.ts: -------------------------------------------------------------------------------- 1 | export function formatNumber(value: number, decimals: number = 0): string { 2 | return Number(value).toLocaleString(undefined, { 3 | minimumFractionDigits: decimals > 0 ? decimals : 0, 4 | maximumFractionDigits: decimals > 0 ? decimals : 0, 5 | }); 6 | } 7 | -------------------------------------------------------------------------------- /src/pages/base/ScrollToTop.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { useLocation } from 'react-router-dom'; 3 | 4 | export function ScrollToTop(): null { 5 | const { pathname } = useLocation(); 6 | 7 | useEffect(() => { 8 | window.scrollTo(0, 0); 9 | }, [pathname]); 10 | 11 | return null; 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /src/utils/axios.ts: -------------------------------------------------------------------------------- 1 | import { Axios } from 'axios'; 2 | import { useStore } from '../store'; 3 | 4 | export function createAxios(): Axios { 5 | const idToken = useStore.getState().idToken; 6 | 7 | if (idToken === undefined) { 8 | return new Axios(); 9 | } 10 | 11 | return new Axios({ 12 | headers: { 13 | authorization: `Bearer ${idToken}`, 14 | }, 15 | validateStatus: status => status >= 200 && status < 300, 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /src/pages/visualizer/OrderDepthTableSpreadRow.tsx: -------------------------------------------------------------------------------- 1 | import { formatNumber } from '../../utils/format'; 2 | 3 | export interface OrderDepthTableSpreadRowProps { 4 | spread: number; 5 | } 6 | 7 | export function OrderDepthTableSpreadRow({ spread }: OrderDepthTableSpreadRowProps): JSX.Element { 8 | return ( 9 | 10 | 11 | ↑ {formatNumber(spread)} ↓ 12 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/pages/base/PrismScrollArea.tsx: -------------------------------------------------------------------------------- 1 | import { ScrollArea } from '@mantine/core'; 2 | import { ReactNode } from 'react'; 3 | 4 | export interface PrismScrollAreaProps { 5 | children: ReactNode; 6 | } 7 | 8 | export function PrismScrollArea({ children }: PrismScrollAreaProps): JSX.Element { 9 | return ( 10 |
11 | {children} 12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/pages/home/HomeCard.tsx: -------------------------------------------------------------------------------- 1 | import { Paper, 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): JSX.Element { 10 | return ( 11 | 12 | 13 | {title} 14 | 15 | 16 | {children} 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/pages/home/ErrorAlert.tsx: -------------------------------------------------------------------------------- 1 | import { Alert, AlertProps } from '@mantine/core'; 2 | import { IconAlertCircle } from '@tabler/icons-react'; 3 | 4 | export interface ErrorAlertProps extends Partial { 5 | error: Error; 6 | } 7 | 8 | export function ErrorAlert({ error, ...alertProps }: ErrorAlertProps): JSX.Element { 9 | return ( 10 | } title="Error" color="red" {...alertProps}> 11 | {error.message} 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/pages/visualizer/VisualizerCard.tsx: -------------------------------------------------------------------------------- 1 | import { Paper, PaperProps, Title } from '@mantine/core'; 2 | 3 | interface VisualizerCardProps extends PaperProps { 4 | title?: string; 5 | } 6 | 7 | export function VisualizerCard({ title, children, ...paperProps }: VisualizerCardProps): JSX.Element { 8 | return ( 9 | 10 | {title && ( 11 | 12 | {title} 13 | 14 | )} 15 | 16 | {children} 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext", "ES2016"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /src/pages/visualizer/SimpleTable.tsx: -------------------------------------------------------------------------------- 1 | import { Table, Text } from '@mantine/core'; 2 | 3 | export interface SimpleTableProps { 4 | label: string; 5 | columns: string[]; 6 | rows: JSX.Element[]; 7 | } 8 | 9 | export function SimpleTable({ label, columns, rows }: SimpleTableProps): JSX.Element { 10 | if (rows.length === 0) { 11 | return Timestamp has no {label}; 12 | } 13 | 14 | return ( 15 | 16 | 17 | 18 | {columns.map((column, i) => ( 19 | 20 | ))} 21 | 22 | 23 | {rows} 24 |
{column}
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/pages/visualizer/ObservationChart.tsx: -------------------------------------------------------------------------------- 1 | import Highcharts from 'highcharts'; 2 | import { Product } from '../../models'; 3 | import { useStore } from '../../store'; 4 | import { Chart } from './Chart'; 5 | 6 | export interface ObservationChartProps { 7 | product: Product; 8 | } 9 | 10 | export function ObservationChart({ product }: ObservationChartProps): JSX.Element { 11 | const algorithm = useStore(state => state.algorithm)!; 12 | 13 | const series: Highcharts.SeriesOptionsType[] = [ 14 | { 15 | type: 'line', 16 | name: 'Value', 17 | data: algorithm.sandboxLogs.map(row => [row.state.timestamp, row.state.observations[product]]), 18 | }, 19 | ]; 20 | 21 | return ; 22 | } 23 | -------------------------------------------------------------------------------- /src/pages/home/AlgorithmList.tsx: -------------------------------------------------------------------------------- 1 | import { Accordion, Text } from '@mantine/core'; 2 | import { AlgorithmSummary } from '../../models'; 3 | import { AlgorithmDetail } from './AlgorithmDetail'; 4 | 5 | export interface AlgorithmListProps { 6 | algorithms: AlgorithmSummary[]; 7 | } 8 | 9 | export function AlgorithmList({ algorithms }: AlgorithmListProps): JSX.Element { 10 | if (algorithms.length === 0) { 11 | return No algorithms found; 12 | } 13 | 14 | return ( 15 | 16 | {algorithms.map((algorithm, i) => ( 17 | 18 | ))} 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/pages/visualizer/ListingTable.tsx: -------------------------------------------------------------------------------- 1 | import { TradingState } from '../../models'; 2 | import { SimpleTable } from './SimpleTable'; 3 | 4 | export interface ListingTableProps { 5 | listings: TradingState['listings']; 6 | } 7 | 8 | export function ListingTable({ listings }: ListingTableProps): JSX.Element { 9 | const rows: JSX.Element[] = []; 10 | for (const symbol of Object.keys(listings)) { 11 | const listing = listings[symbol]; 12 | 13 | rows.push( 14 | 15 | {listing.symbol} 16 | {listing.product} 17 | {listing.denomination} 18 | , 19 | ); 20 | } 21 | 22 | return ; 23 | } 24 | -------------------------------------------------------------------------------- /src/pages/visualizer/SubmissionLogsCard.tsx: -------------------------------------------------------------------------------- 1 | import { Prism } from '@mantine/prism'; 2 | import { useStore } from '../../store'; 3 | import { PrismScrollArea } from '../base/PrismScrollArea'; 4 | import { VisualizerCard } from './VisualizerCard'; 5 | 6 | export function SubmissionLogsCard(): JSX.Element { 7 | const algorithm = useStore(state => state.algorithm)!; 8 | 9 | if (algorithm.submissionLogs === '') { 10 | return Algorithm has no submission logs; 11 | } 12 | 13 | return ( 14 | 15 | 16 | {algorithm.submissionLogs} 17 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IMC Prosperity Visualizer 2 | 3 | [![Build Status](https://github.com/jmerle/imc-prosperity-visualizer/workflows/Build/badge.svg)](https://github.com/jmerle/imc-prosperity-visualizer/actions/workflows/build.yml) 4 | 5 | [IMC Prosperity Visualizer](https://jmerle.github.io/imc-prosperity-visualizer/) is a visualizer for [IMC Prosperity 1](https://prosperity.imc.com/) algorithms. It is available at [https://jmerle.github.io/imc-prosperity-visualizer/](https://jmerle.github.io/imc-prosperity-visualizer/). 6 | 7 | Please note that this visualizer only supports IMC Prosperity 1 algorithms. See my [IMC Prosperity 2 Visualizer](https://github.com/jmerle/imc-prosperity-2-visualizer) for IMC Prosperity 2 algorithms. 8 | 9 | ![](https://i.imgur.com/I1qG6qq.png) 10 | 11 | ![](https://i.imgur.com/8CJfgea.png) 12 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: [push, pull_request] 4 | 5 | permissions: 6 | contents: write 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v3 15 | 16 | - name: Set up Node.js 17 | uses: actions/setup-node@v3 18 | with: 19 | node-version: 18 20 | cache: yarn 21 | 22 | - name: Install dependencies 23 | run: yarn --frozen-lockfile 24 | 25 | - name: Lint 26 | run: yarn lint 27 | 28 | - name: Build 29 | run: yarn build && cp 404.html dist/404.html 30 | 31 | - name: Deploy 32 | if: github.event_name == 'push' && github.ref == 'refs/heads/master' 33 | uses: JamesIves/github-pages-deploy-action@v4 34 | with: 35 | folder: dist 36 | git-config-name: GitHub Actions 37 | git-config-email: actions@github.com 38 | -------------------------------------------------------------------------------- /src/pages/visualizer/PositionTable.tsx: -------------------------------------------------------------------------------- 1 | import { TradingState } from '../../models'; 2 | import { getAskColor, getBidColor } from '../../utils/colors'; 3 | import { formatNumber } from '../../utils/format'; 4 | import { SimpleTable } from './SimpleTable'; 5 | 6 | export interface PositionTableProps { 7 | position: TradingState['position']; 8 | } 9 | 10 | export function PositionTable({ position }: PositionTableProps): JSX.Element { 11 | const rows: JSX.Element[] = []; 12 | for (const product of Object.keys(position)) { 13 | if (position[product] === 0) { 14 | continue; 15 | } 16 | 17 | const colorFunc = position[product] > 0 ? getBidColor : getAskColor; 18 | 19 | rows.push( 20 | 21 | {product} 22 | {formatNumber(position[product])} 23 | , 24 | ); 25 | } 26 | 27 | return ; 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 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/store.tsx: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand'; 2 | import { persist } from 'zustand/middleware'; 3 | import { Algorithm, Theme } from './models'; 4 | 5 | export interface State { 6 | theme: Theme; 7 | idToken: string; 8 | round: string; 9 | 10 | algorithm: Algorithm | null; 11 | 12 | setTheme: (theme: Theme) => void; 13 | setIdToken: (idToken: string) => void; 14 | setRound: (round: string) => void; 15 | setAlgorithm: (algorithm: Algorithm | null) => void; 16 | } 17 | 18 | export const useStore = create( 19 | persist( 20 | set => ({ 21 | theme: 'system', 22 | idToken: '', 23 | round: 'ROUND0', 24 | 25 | algorithm: null, 26 | 27 | setTheme: theme => set({ theme }), 28 | setIdToken: idToken => set({ idToken }), 29 | setRound: round => set({ round }), 30 | setAlgorithm: algorithm => set({ algorithm }), 31 | }), 32 | { 33 | name: 'imc-prosperity-visualizer', 34 | partialize: state => 35 | ({ 36 | theme: state.theme, 37 | idToken: state.idToken, 38 | round: state.round, 39 | } as State), 40 | }, 41 | ), 42 | ); 43 | -------------------------------------------------------------------------------- /src/pages/visualizer/OrderTable.tsx: -------------------------------------------------------------------------------- 1 | import { SandboxLogRow } from '../../models'; 2 | import { getAskColor, getBidColor } from '../../utils/colors'; 3 | import { formatNumber } from '../../utils/format'; 4 | import { SimpleTable } from './SimpleTable'; 5 | 6 | export interface OrderTableProps { 7 | orders: SandboxLogRow['orders']; 8 | } 9 | 10 | export function OrderTable({ orders }: OrderTableProps): JSX.Element { 11 | const rows: JSX.Element[] = []; 12 | for (const symbol of Object.keys(orders)) { 13 | for (let i = 0; i < orders[symbol].length; i++) { 14 | const order = orders[symbol][i]; 15 | 16 | const colorFunc = order.quantity > 0 ? getBidColor : getAskColor; 17 | 18 | rows.push( 19 | 20 | {order.symbol} 21 | {order.quantity > 0 ? 'Buy' : 'Sell'} 22 | {formatNumber(order.price)} 23 | {formatNumber(Math.abs(order.quantity))} 24 | , 25 | ); 26 | } 27 | } 28 | 29 | return ; 30 | } 31 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { MantineProvider } from '@mantine/core'; 2 | import { useColorScheme } from '@mantine/hooks'; 3 | import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom'; 4 | import { Page } from './pages/base/Page'; 5 | import { HomePage } from './pages/home/HomePage'; 6 | import { VisualizerPage } from './pages/visualizer/VisualizerPage'; 7 | import { useStore } from './store'; 8 | 9 | export function App(): JSX.Element { 10 | const theme = useStore(state => state.theme); 11 | const preferredColorScheme = useColorScheme(); 12 | 13 | const colorScheme = theme === 'system' ? preferredColorScheme : theme; 14 | 15 | return ( 16 | 17 | 18 | 19 | }> 20 | } /> 21 | } /> 22 | 23 | } /> 24 | 25 | 26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /src/pages/base/ThemeSwitch.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Center, SegmentedControl } from '@mantine/core'; 2 | import { IconDeviceDesktop, IconMoon, IconSun } from '@tabler/icons-react'; 3 | import { useStore } from '../../store'; 4 | 5 | export function ThemeSwitch(): JSX.Element { 6 | const theme = useStore(state => state.theme); 7 | const setTheme = useStore(state => state.setTheme); 8 | 9 | return ( 10 | 17 | 18 | Light 19 | 20 | ), 21 | value: 'light', 22 | }, 23 | { 24 | label: ( 25 |
26 | 27 | Dark 28 |
29 | ), 30 | value: 'dark', 31 | }, 32 | { 33 | label: ( 34 |
35 | 36 | System 37 |
38 | ), 39 | value: 'system', 40 | }, 41 | ]} 42 | /> 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /src/utils/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/ProfitLossTable.tsx: -------------------------------------------------------------------------------- 1 | import { useStore } from '../../store'; 2 | import { getAskColor, getBidColor } from '../../utils/colors'; 3 | import { formatNumber } from '../../utils/format'; 4 | import { SimpleTable } from './SimpleTable'; 5 | 6 | export interface ProfitLossTableProps { 7 | timestamp: number; 8 | } 9 | 10 | export function ProfitLossTable({ timestamp }: ProfitLossTableProps): JSX.Element { 11 | const algorithm = useStore(state => state.algorithm)!; 12 | 13 | const rows: JSX.Element[] = algorithm.activityLogs 14 | .filter(row => row.timestamp === timestamp) 15 | .filter(row => algorithm.sandboxLogs[0].state.observations[row.product] === undefined) 16 | .sort((a, b) => a.product.localeCompare(b.product)) 17 | .map(row => { 18 | let colorFunc: (alpha: number) => string = () => 'transparent'; 19 | if (row.profitLoss > 0) { 20 | colorFunc = getBidColor; 21 | } else if (row.profitLoss < 0) { 22 | colorFunc = getAskColor; 23 | } 24 | 25 | return ( 26 | 27 | {row.product} 28 | {formatNumber(row.profitLoss)} 29 | 30 | ); 31 | }); 32 | 33 | return ; 34 | } 35 | -------------------------------------------------------------------------------- /src/pages/visualizer/TradeTable.tsx: -------------------------------------------------------------------------------- 1 | import { ProsperitySymbol, Trade } from '../../models'; 2 | import { getAskColor, getBidColor } from '../../utils/colors'; 3 | import { formatNumber } from '../../utils/format'; 4 | import { SimpleTable } from './SimpleTable'; 5 | 6 | export interface TradeTableProps { 7 | trades: Record; 8 | } 9 | 10 | export function TradeTable({ trades }: TradeTableProps): JSX.Element { 11 | const rows: JSX.Element[] = []; 12 | for (const symbol of Object.keys(trades).sort((a, b) => a.localeCompare(b))) { 13 | for (let i = 0; i < trades[symbol].length; i++) { 14 | const trade = trades[symbol][i]; 15 | 16 | let color: string; 17 | if (trade.buyer === 'SUBMISSION') { 18 | color = getBidColor(0.1); 19 | } else if (trade.seller === 'SUBMISSION') { 20 | color = getAskColor(0.1); 21 | } else { 22 | color = 'transparent'; 23 | } 24 | 25 | rows.push( 26 | 27 | {trade.symbol} 28 | {trade.buyer} 29 | {trade.seller} 30 | {formatNumber(trade.price)} 31 | {formatNumber(trade.quantity)} 32 | {formatNumber(trade.timestamp)} 33 | , 34 | ); 35 | } 36 | } 37 | 38 | return ( 39 | 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /src/pages/visualizer/ProfitLossChart.tsx: -------------------------------------------------------------------------------- 1 | import Highcharts from 'highcharts'; 2 | import { useStore } from '../../store'; 3 | import { Chart } from './Chart'; 4 | 5 | export function ProfitLossChart(): JSX.Element { 6 | const algorithm = useStore(state => state.algorithm)!; 7 | 8 | const dataByTimestamp = new Map(); 9 | for (const row of algorithm.activityLogs) { 10 | if (!dataByTimestamp.has(row.timestamp)) { 11 | dataByTimestamp.set(row.timestamp, row.profitLoss); 12 | } else { 13 | dataByTimestamp.set(row.timestamp, dataByTimestamp.get(row.timestamp)! + row.profitLoss); 14 | } 15 | } 16 | 17 | const series: Highcharts.SeriesOptionsType[] = [ 18 | { 19 | type: 'line', 20 | name: 'Total', 21 | data: [...dataByTimestamp.keys()].map(timestamp => [timestamp, dataByTimestamp.get(timestamp)]), 22 | }, 23 | ]; 24 | 25 | Object.keys(algorithm.sandboxLogs[0].state.listings) 26 | .filter(key => algorithm.sandboxLogs[0].state.observations[key] === undefined) 27 | .sort((a, b) => a.localeCompare(b)) 28 | .forEach(symbol => { 29 | const data = []; 30 | 31 | for (const row of algorithm.activityLogs) { 32 | if (row.product === symbol) { 33 | data.push([row.timestamp, row.profitLoss]); 34 | } 35 | } 36 | 37 | series.push({ 38 | type: 'line', 39 | name: symbol, 40 | data, 41 | dashStyle: 'Dash', 42 | }); 43 | }); 44 | 45 | return ; 46 | } 47 | -------------------------------------------------------------------------------- /src/pages/visualizer/VolumeChart.tsx: -------------------------------------------------------------------------------- 1 | import Highcharts from 'highcharts'; 2 | import { ProsperitySymbol } from '../../models'; 3 | import { useStore } from '../../store'; 4 | import { getAskColor, getBidColor } from '../../utils/colors'; 5 | import { Chart } from './Chart'; 6 | 7 | export interface VolumeChartProps { 8 | symbol: ProsperitySymbol; 9 | } 10 | 11 | export function VolumeChart({ symbol }: VolumeChartProps): JSX.Element { 12 | const algorithm = useStore(state => state.algorithm)!; 13 | 14 | const series: Highcharts.SeriesOptionsType[] = [ 15 | { type: 'column', name: 'Bid 3', color: getBidColor(0.5), data: [] }, 16 | { type: 'column', name: 'Bid 2', color: getBidColor(0.75), data: [] }, 17 | { type: 'column', name: 'Bid 1', color: getBidColor(1.0), data: [] }, 18 | { type: 'column', name: 'Ask 1', color: getAskColor(1.0), data: [] }, 19 | { type: 'column', name: 'Ask 2', color: getAskColor(0.75), data: [] }, 20 | { type: 'column', name: 'Ask 3', color: getAskColor(0.5), data: [] }, 21 | ]; 22 | 23 | for (const row of algorithm.activityLogs) { 24 | if (row.product !== symbol) { 25 | continue; 26 | } 27 | 28 | for (let i = 0; i < row.bidVolumes.length; i++) { 29 | (series[2 - i] as any).data.push([row.timestamp, row.bidVolumes[i]]); 30 | } 31 | 32 | for (let i = 0; i < row.askVolumes.length; i++) { 33 | (series[i + 3] as any).data.push([row.timestamp, row.askVolumes[i]]); 34 | } 35 | } 36 | 37 | return ; 38 | } 39 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | IMC Prosperity Visualizer 9 | 10 | 11 | 35 | 36 | 37 | 38 |
39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/pages/visualizer/PriceChart.tsx: -------------------------------------------------------------------------------- 1 | import Highcharts from 'highcharts'; 2 | import { ProsperitySymbol } from '../../models'; 3 | import { useStore } from '../../store'; 4 | import { getAskColor, getBidColor } from '../../utils/colors'; 5 | import { Chart } from './Chart'; 6 | 7 | export interface PriceChartProps { 8 | symbol: ProsperitySymbol; 9 | } 10 | 11 | export function PriceChart({ symbol }: PriceChartProps): JSX.Element { 12 | const algorithm = useStore(state => state.algorithm)!; 13 | 14 | const series: Highcharts.SeriesOptionsType[] = [ 15 | { type: 'line', name: 'Bid 3', color: getBidColor(0.5), marker: { symbol: 'square' }, data: [] }, 16 | { type: 'line', name: 'Bid 2', color: getBidColor(0.75), marker: { symbol: 'circle' }, data: [] }, 17 | { type: 'line', name: 'Bid 1', color: getBidColor(1.0), marker: { symbol: 'triangle' }, data: [] }, 18 | { type: 'line', name: 'Mid price', color: 'gray', dashStyle: 'Dash', marker: { symbol: 'diamond' }, data: [] }, 19 | { type: 'line', name: 'Ask 1', color: getAskColor(1.0), marker: { symbol: 'triangle-down' }, data: [] }, 20 | { type: 'line', name: 'Ask 2', color: getAskColor(0.75), marker: { symbol: 'circle' }, data: [] }, 21 | { type: 'line', name: 'Ask 3', color: getAskColor(0.5), marker: { symbol: 'square' }, 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.bidPrices.length; i++) { 30 | (series[2 - i] as any).data.push([row.timestamp, row.bidPrices[i]]); 31 | } 32 | 33 | (series[3] as any).data.push([row.timestamp, row.midPrice]); 34 | 35 | for (let i = 0; i < row.askPrices.length; i++) { 36 | (series[i + 4] as any).data.push([row.timestamp, row.askPrices[i]]); 37 | } 38 | } 39 | 40 | return ; 41 | } 42 | -------------------------------------------------------------------------------- /404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Single Page Apps for GitHub Pages 6 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/pages/visualizer/SandboxLogsCard.tsx: -------------------------------------------------------------------------------- 1 | import { Slider, SliderProps, Text } from '@mantine/core'; 2 | import { useHotkeys } from '@mantine/hooks'; 3 | import { useState } from 'react'; 4 | import { SandboxLogRow } from '../../models'; 5 | import { useStore } from '../../store'; 6 | import { formatNumber } from '../../utils/format'; 7 | import { SandboxLogDetail } from './SandboxLogDetail'; 8 | import { VisualizerCard } from './VisualizerCard'; 9 | 10 | export function SandboxLogsCard(): JSX.Element { 11 | const algorithm = useStore(state => state.algorithm)!; 12 | 13 | const rowsByTimestamp: Record = {}; 14 | for (const row of algorithm.sandboxLogs) { 15 | rowsByTimestamp[row.state.timestamp] = row; 16 | } 17 | 18 | const timestampMin = algorithm.sandboxLogs[0].state.timestamp; 19 | const timestampMax = algorithm.sandboxLogs[algorithm.sandboxLogs.length - 1].state.timestamp; 20 | const timestampStep = algorithm.sandboxLogs[1].state.timestamp - algorithm.sandboxLogs[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 { Algorithm, ProsperitySymbol } from '../../models'; 3 | import { useStore } from '../../store'; 4 | import { Chart } from './Chart'; 5 | 6 | function getLimit(algorithm: Algorithm, symbol: ProsperitySymbol): number { 7 | const knownLimits: Record = { 8 | PEARLS: 20, 9 | BANANAS: 20, 10 | COCONUTS: 600, 11 | PINA_COLADAS: 300, 12 | DIVING_GEAR: 50, 13 | BERRIES: 250, 14 | BAGUETTE: 150, 15 | DIP: 300, 16 | UKULELE: 70, 17 | PICNIC_BASKET: 70, 18 | }; 19 | 20 | if (knownLimits[symbol] !== undefined) { 21 | return knownLimits[symbol]; 22 | } 23 | 24 | // This code will be hit when a new product is added to the competition and the visualizer isn't updated yet 25 | // In that case the visualizer doesn't know the real limit yet, so we make a guess based on the algorithm's positions 26 | 27 | const product = algorithm.sandboxLogs[0].state.listings[symbol].product; 28 | 29 | const positions = algorithm.sandboxLogs.map(row => row.state.position[product] || 0); 30 | const minPosition = Math.min(...positions); 31 | const maxPosition = Math.max(...positions); 32 | 33 | return Math.max(Math.abs(minPosition), maxPosition); 34 | } 35 | 36 | export function PositionChart(): JSX.Element { 37 | const algorithm = useStore(state => state.algorithm)!; 38 | 39 | const symbols = Object.keys(algorithm.sandboxLogs[0].state.listings) 40 | .filter(key => algorithm.sandboxLogs[0].state.observations[key] === undefined) 41 | .sort((a, b) => a.localeCompare(b)); 42 | 43 | const limits: Record = {}; 44 | for (const symbol of symbols) { 45 | limits[symbol] = getLimit(algorithm, symbol); 46 | } 47 | 48 | const data: Record = {}; 49 | for (const symbol of symbols) { 50 | data[symbol] = []; 51 | } 52 | 53 | for (const row of algorithm.sandboxLogs) { 54 | for (const symbol of symbols) { 55 | const position = row.state.position[symbol] || 0; 56 | data[symbol].push([row.state.timestamp, (position / limits[symbol]) * 100]); 57 | } 58 | } 59 | 60 | const series: Highcharts.SeriesOptionsType[] = symbols.map(symbol => ({ 61 | type: 'line', 62 | name: symbol, 63 | data: data[symbol], 64 | })); 65 | 66 | return ; 67 | } 68 | -------------------------------------------------------------------------------- /src/pages/visualizer/OrderDepthTable.tsx: -------------------------------------------------------------------------------- 1 | import { Table, Text } from '@mantine/core'; 2 | import { OrderDepth } from '../../models'; 3 | import { getAskColor, getBidColor } from '../../utils/colors'; 4 | import { formatNumber } from '../../utils/format'; 5 | import { OrderDepthTableSpreadRow } from './OrderDepthTableSpreadRow'; 6 | 7 | export interface OrderDepthTableProps { 8 | orderDepth: OrderDepth; 9 | } 10 | 11 | export function OrderDepthTable({ orderDepth }: OrderDepthTableProps): JSX.Element { 12 | const rows: JSX.Element[] = []; 13 | 14 | const askPrices = Object.keys(orderDepth.sell_orders) 15 | .map(Number) 16 | .sort((a, b) => b - a); 17 | const bidPrices = Object.keys(orderDepth.buy_orders) 18 | .map(Number) 19 | .sort((a, b) => b - a); 20 | 21 | for (let i = 0; i < askPrices.length; i++) { 22 | const price = askPrices[i]; 23 | 24 | if (i > 0 && askPrices[i - 1] - price > 1) { 25 | rows.push(); 26 | } 27 | 28 | rows.push( 29 | 30 | 31 | {formatNumber(price)} 32 | {formatNumber(Math.abs(orderDepth.sell_orders[price]))} 33 | , 34 | ); 35 | } 36 | 37 | if (askPrices.length > 0 && bidPrices.length > 0) { 38 | rows.push(); 39 | } 40 | 41 | for (let i = 0; i < bidPrices.length; i++) { 42 | const price = bidPrices[i]; 43 | 44 | if (i > 0 && bidPrices[i - 1] - price > 1) { 45 | rows.push(); 46 | } 47 | 48 | rows.push( 49 | 50 | 51 | {formatNumber(orderDepth.buy_orders[price])} 52 | 53 | {formatNumber(price)} 54 | 55 | , 56 | ); 57 | } 58 | 59 | if (rows.length === 0) { 60 | return Timestamp has no order depth; 61 | } 62 | 63 | return ( 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | {rows} 73 |
Bid volumePriceAsk volume
74 | ); 75 | } 76 | -------------------------------------------------------------------------------- /src/pages/visualizer/AlgorithmSummaryCard.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Grid, Group, Text, Title } from '@mantine/core'; 2 | import { Prism } from '@mantine/prism'; 3 | import { format } from 'date-fns'; 4 | import { useStore } from '../../store'; 5 | import { downloadAlgorithmResults } from '../../utils/algorithm'; 6 | import { useAsync } from '../../utils/async'; 7 | import { PrismScrollArea } from '../base/PrismScrollArea'; 8 | import { VisualizerCard } from './VisualizerCard'; 9 | 10 | export function AlgorithmSummaryCard(): JSX.Element { 11 | const algorithm = useStore(state => state.algorithm)!; 12 | const summary = algorithm.summary!; 13 | 14 | const timestamp = format(Date.parse(summary.timestamp), 'yyyy-MM-dd HH:mm:ss'); 15 | 16 | const downloadResults = useAsync(async () => { 17 | await downloadAlgorithmResults(summary.id); 18 | }); 19 | 20 | return ( 21 | 22 | 23 | 24 | Id 25 | {summary.id} 26 | 27 | 28 | File name 29 | {summary.fileName} 30 | 31 | 32 | Submitted at 33 | {timestamp} 34 | 35 | 36 | Submitted by 37 | 38 | {summary.user.firstName} {summary.user.lastName} 39 | 40 | 41 | 42 | Status 43 | {summary.status} 44 | 45 | 46 | Round 47 | {summary.round} 48 | 49 | 50 | Selected for round 51 | {summary.selectedForRound ? 'Yes' : 'No'} 52 | 53 | 54 | Content 55 | 56 | {summary.content} 57 | 58 | 59 | 60 | 61 | 71 | 74 | 75 | 76 | 77 | 78 | ); 79 | } 80 | -------------------------------------------------------------------------------- /src/pages/home/LoadFromElsewhere.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Code, Text, TextInput } from '@mantine/core'; 2 | import { useScrollIntoView } from '@mantine/hooks'; 3 | import axios from 'axios'; 4 | import { FormEvent, useCallback, useEffect, useState } from 'react'; 5 | import { useNavigate, useSearchParams } from 'react-router-dom'; 6 | import { useStore } from '../../store'; 7 | import { parseAlgorithmLogs } from '../../utils/algorithm'; 8 | import { useAsync } from '../../utils/async'; 9 | import { ErrorAlert } from './ErrorAlert'; 10 | import { HomeCard } from './HomeCard'; 11 | 12 | export function LoadFromElsewhere(): JSX.Element { 13 | const [url, setUrl] = useState(''); 14 | 15 | const { scrollIntoView, targetRef } = useScrollIntoView({ 16 | duration: 0, 17 | }); 18 | 19 | const algorithm = useStore(state => state.algorithm); 20 | const setAlgorithm = useStore(state => state.setAlgorithm); 21 | 22 | const navigate = useNavigate(); 23 | const searchParams = useSearchParams()[0]; 24 | 25 | const loadAlgorithm = useAsync(async (logsUrl: string): Promise => { 26 | const logsResponse = await axios.get(logsUrl); 27 | setAlgorithm(parseAlgorithmLogs(logsResponse.data)); 28 | navigate(`/visualizer?open=${logsUrl}`); 29 | }); 30 | 31 | const onSubmit = useCallback( 32 | (event?: FormEvent) => { 33 | event?.preventDefault(); 34 | 35 | if (url.trim().length > 0) { 36 | loadAlgorithm.call(url); 37 | } 38 | }, 39 | [loadAlgorithm], 40 | ); 41 | 42 | useEffect(() => { 43 | if (algorithm !== null || loadAlgorithm.loading) { 44 | return; 45 | } 46 | 47 | if (!searchParams.has('open')) { 48 | return; 49 | } 50 | 51 | const url = searchParams.get('open') || ''; 52 | 53 | setUrl(url); 54 | 55 | if (url.trim().length > 0) { 56 | scrollIntoView(); 57 | loadAlgorithm.call(url); 58 | } 59 | }, []); 60 | 61 | return ( 62 |
63 | 64 | 65 | Supports URLs to log files that are in the same format as the ones generated by the Prosperity servers. This 66 | format is undocumented, but you can get an idea of what it looks like by downloading a log file from a 67 | submitted algorithm. The URL must allow cross-origin requests from the visualizer's website. 68 | 69 | 70 | {/* prettier-ignore */} 71 | 72 | This input type can also be used by browsing to {window.location.origin}{window.location.pathname}?open=<url>. 73 | 74 | 75 | {loadAlgorithm.error && } 76 | 77 |
78 | setUrl((e.target as HTMLInputElement).value)} 83 | mt="xs" 84 | /> 85 | 86 | 89 | 90 |
91 |
92 | ); 93 | } 94 | -------------------------------------------------------------------------------- /src/pages/visualizer/SandboxLogDetail.tsx: -------------------------------------------------------------------------------- 1 | import { Grid, Text, Title } from '@mantine/core'; 2 | import { Prism } from '@mantine/prism'; 3 | import { SandboxLogRow } from '../../models'; 4 | import { useStore } from '../../store'; 5 | import { formatNumber } from '../../utils/format'; 6 | import { PrismScrollArea } from '../base/PrismScrollArea'; 7 | import { ListingTable } from './ListingTable'; 8 | import { OrderDepthTable } from './OrderDepthTable'; 9 | import { OrderTable } from './OrderTable'; 10 | import { PositionTable } from './PositionTable'; 11 | import { ProfitLossTable } from './ProfitLossTable'; 12 | import { TradeTable } from './TradeTable'; 13 | 14 | export interface SandboxLogDetailProps { 15 | row: SandboxLogRow; 16 | } 17 | 18 | export function SandboxLogDetail({ row: { state, orders, logs } }: SandboxLogDetailProps): JSX.Element { 19 | const algorithm = useStore(state => state.algorithm)!; 20 | 21 | const profitLoss = algorithm.activityLogs 22 | .filter(row => row.timestamp === state.timestamp) 23 | .reduce((acc, val) => acc + val.profitLoss, 0); 24 | 25 | let orderDepthWidth = 3; 26 | const orderDepthCount = Object.keys(state.order_depths).length; 27 | if (orderDepthCount % 4 > 0 && orderDepthCount % 3 === 0) { 28 | orderDepthWidth = 4; 29 | } 30 | 31 | return ( 32 | 33 | 34 | 35 | Timestamp {formatNumber(state.timestamp)} • Profit / Loss: {formatNumber(profitLoss)} 36 | 37 | 38 | 39 | Listings 40 | 41 | 42 | 43 | Positions 44 | 45 | 46 | 47 | Profit / Loss 48 | 49 | 50 | {Object.keys(state.order_depths).map((symbol, i) => ( 51 | 52 | {symbol} order depth 53 | 54 | 55 | ))} 56 | {Object.keys(state.order_depths).length % (12 / orderDepthWidth) > 0 && } 57 | 58 | Own trades 59 | {} 60 | 61 | 62 | Market trades 63 | {} 64 | 65 | 66 | Orders 67 | {} 68 | 69 | 70 | Logs 71 | {logs ? ( 72 | 73 | {logs} 74 | 75 | ) : ( 76 | Timestamp has no logs 77 | )} 78 | 79 | 80 | ); 81 | } 82 | -------------------------------------------------------------------------------- /src/models.ts: -------------------------------------------------------------------------------- 1 | import { ColorScheme } from '@mantine/core'; 2 | 3 | export type Theme = ColorScheme | 'system'; 4 | 5 | export interface UserSummary { 6 | id: number; 7 | firstName: string; 8 | lastName: string; 9 | } 10 | 11 | export interface AlgorithmSummary { 12 | id: string; 13 | content: string; 14 | fileName: string; 15 | round: string; 16 | selectedForRound: boolean; 17 | status: string; 18 | teamId: string; 19 | timestamp: string; 20 | user: UserSummary; 21 | } 22 | 23 | export type Time = number; 24 | export type ProsperitySymbol = string; 25 | export type Product = string; 26 | export type Position = number; 27 | export type UserId = string; 28 | export type Observation = number; 29 | 30 | export interface ActivityLogRow { 31 | day: number; 32 | timestamp: number; 33 | product: Product; 34 | bidPrices: number[]; 35 | bidVolumes: number[]; 36 | askPrices: number[]; 37 | askVolumes: number[]; 38 | midPrice: number; 39 | profitLoss: number; 40 | } 41 | 42 | export interface Listing { 43 | symbol: ProsperitySymbol; 44 | product: Product; 45 | denomination: Product; 46 | } 47 | 48 | export interface Order { 49 | symbol: ProsperitySymbol; 50 | price: number; 51 | quantity: number; 52 | } 53 | 54 | export interface OrderDepth { 55 | buy_orders: Record; 56 | sell_orders: Record; 57 | } 58 | 59 | export interface Trade { 60 | symbol: ProsperitySymbol; 61 | price: number; 62 | quantity: number; 63 | buyer: UserId; 64 | seller: UserId; 65 | timestamp: Time; 66 | } 67 | 68 | export interface TradingState { 69 | timestamp: Time; 70 | listings: Record; 71 | order_depths: Record; 72 | own_trades: Record; 73 | market_trades: Record; 74 | position: Record; 75 | observations: Record; 76 | } 77 | 78 | export interface SandboxLogRow { 79 | state: TradingState; 80 | orders: Record; 81 | logs: string; 82 | } 83 | 84 | export interface Algorithm { 85 | summary?: AlgorithmSummary; 86 | activityLogs: ActivityLogRow[]; 87 | sandboxLogs: SandboxLogRow[]; 88 | submissionLogs: string; 89 | } 90 | 91 | export type CompressedListing = [symbol: ProsperitySymbol, product: Product, denomination: Product]; 92 | 93 | export type CompressedOrderDepth = [buy_orders: Record, sell_orders: Record]; 94 | 95 | export type CompressedTrade = [ 96 | symbol: ProsperitySymbol, 97 | buyer: UserId, 98 | seller: UserId, 99 | price: number, 100 | quantity: number, 101 | timestamp: Time, 102 | ]; 103 | 104 | export interface CompressedTradingState { 105 | t: Time; 106 | l: CompressedListing[]; 107 | od: Record; 108 | ot: CompressedTrade[]; 109 | mt: CompressedTrade[]; 110 | p: Record; 111 | o: Record; 112 | } 113 | 114 | export type CompressedOrder = [symbol: ProsperitySymbol, price: number, quantity: number]; 115 | 116 | export interface CompressedSandboxLogRow { 117 | state: CompressedTradingState; 118 | orders: CompressedOrder[]; 119 | logs: string; 120 | } 121 | -------------------------------------------------------------------------------- /src/pages/home/LoadFromFile.tsx: -------------------------------------------------------------------------------- 1 | import { Group, Text } from '@mantine/core'; 2 | import { Dropzone } from '@mantine/dropzone'; 3 | import { IconUpload } from '@tabler/icons-react'; 4 | import { useCallback, useState } from 'react'; 5 | import { ErrorCode, FileRejection } from 'react-dropzone'; 6 | import { useNavigate } from 'react-router-dom'; 7 | import { useStore } from '../../store'; 8 | import { parseAlgorithmLogs } from '../../utils/algorithm'; 9 | import { useAsync } from '../../utils/async'; 10 | import { ErrorAlert } from './ErrorAlert'; 11 | import { HomeCard } from './HomeCard'; 12 | 13 | function DropzoneContent(): JSX.Element { 14 | return ( 15 | 16 | 17 | 18 | Drag file here or click to select file 19 | 20 | 21 | ); 22 | } 23 | 24 | export function LoadFromFile(): JSX.Element { 25 | const navigate = useNavigate(); 26 | 27 | const [error, setError] = useState(); 28 | 29 | const setAlgorithm = useStore(state => state.setAlgorithm); 30 | 31 | const onDrop = useAsync( 32 | (files: File[]) => 33 | new Promise((resolve, reject) => { 34 | setError(undefined); 35 | 36 | const reader = new FileReader(); 37 | 38 | reader.addEventListener('load', () => { 39 | try { 40 | setAlgorithm(parseAlgorithmLogs(reader.result as string)); 41 | navigate('/visualizer'); 42 | resolve(); 43 | } catch (err: any) { 44 | reject(err); 45 | } 46 | }); 47 | 48 | reader.addEventListener('error', () => { 49 | reject(new Error('FileReader emitted an error event')); 50 | }); 51 | 52 | reader.readAsText(files[0]); 53 | }), 54 | ); 55 | 56 | const onReject = useCallback((rejections: FileRejection[]) => { 57 | const messages: string[] = []; 58 | 59 | for (const rejection of rejections) { 60 | const errorType = { 61 | [ErrorCode.FileInvalidType]: 'Invalid type, only log files are supported.', 62 | [ErrorCode.FileTooLarge]: 'File too large.', 63 | [ErrorCode.FileTooSmall]: 'File too small.', 64 | [ErrorCode.TooManyFiles]: 'Too many files.', 65 | }[rejection.errors[0].code]!; 66 | 67 | messages.push(`Could not load algorithm from ${rejection.file.name}: ${errorType}`); 68 | } 69 | 70 | setError(new Error(messages.join('
'))); 71 | }, []); 72 | 73 | return ( 74 | 75 | 76 | Supports log files that are in the same format as the ones generated by the Prosperity servers. This format is 77 | undocumented, but you can get an idea of what it looks like by downloading a log file from a submitted 78 | algorithm. 79 | 80 | 81 | {error && } 82 | {onDrop.error && } 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | ); 94 | } 95 | -------------------------------------------------------------------------------- /src/pages/visualizer/VisualizerPage.tsx: -------------------------------------------------------------------------------- 1 | import { Center, createStyles, Grid, Title } from '@mantine/core'; 2 | import { Navigate, useLocation } from 'react-router-dom'; 3 | import { useStore } from '../../store'; 4 | import { formatNumber } from '../../utils/format'; 5 | import { AlgorithmSummaryCard } from './AlgorithmSummaryCard'; 6 | import { ObservationChart } from './ObservationChart'; 7 | import { PositionChart } from './PositionChart'; 8 | import { PriceChart } from './PriceChart'; 9 | import { ProfitLossChart } from './ProfitLossChart'; 10 | import { SandboxLogsCard } from './SandboxLogsCard'; 11 | import { SubmissionLogsCard } from './SubmissionLogsCard'; 12 | import { VolumeChart } from './VolumeChart'; 13 | 14 | const useStyles = createStyles(theme => ({ 15 | container: { 16 | margin: '0 auto', 17 | width: '1500px', 18 | 19 | [theme.fn.smallerThan(1500)]: { 20 | width: '100%', 21 | paddingLeft: theme.spacing.md, 22 | paddingRight: theme.spacing.md, 23 | }, 24 | }, 25 | })); 26 | 27 | export function VisualizerPage(): JSX.Element { 28 | const { classes } = useStyles(); 29 | 30 | const algorithm = useStore(state => state.algorithm); 31 | 32 | const { search } = useLocation(); 33 | 34 | if (algorithm === null) { 35 | return ; 36 | } 37 | 38 | let profitLoss = 0; 39 | const lastTimestamp = algorithm.activityLogs[algorithm.activityLogs.length - 1].timestamp; 40 | for (let i = algorithm.activityLogs.length - 1; i >= 0 && algorithm.activityLogs[i].timestamp == lastTimestamp; i--) { 41 | profitLoss += algorithm.activityLogs[i].profitLoss; 42 | } 43 | 44 | const symbolColumns: JSX.Element[] = []; 45 | Object.keys(algorithm.sandboxLogs[0].state.listings) 46 | .filter(key => algorithm.sandboxLogs[0].state.observations[key] === undefined) 47 | .sort((a, b) => a.localeCompare(b)) 48 | .forEach((symbol, i) => { 49 | symbolColumns.push( 50 | 51 | 52 | , 53 | ); 54 | 55 | symbolColumns.push( 56 | 57 | 58 | , 59 | ); 60 | }); 61 | 62 | const observationColumns = Object.keys(algorithm.sandboxLogs[0].state.observations) 63 | .sort((a, b) => a.localeCompare(b)) 64 | .map((product, i) => ( 65 | 66 | 67 | 68 | )); 69 | 70 | if (observationColumns.length % 2 > 0) { 71 | observationColumns.push(); 72 | } 73 | 74 | return ( 75 |
76 | 77 | 78 |
79 | Final Profit / Loss: {formatNumber(profitLoss)} 80 |
81 |
82 | 83 | 84 | 85 | 86 | 87 | 88 | {symbolColumns} 89 | {observationColumns} 90 | 91 | 92 | 93 | 94 | 95 | 96 | {algorithm.summary && ( 97 | 98 | 99 | 100 | )} 101 |
102 |
103 | ); 104 | } 105 | -------------------------------------------------------------------------------- /src/pages/home/LoadFromProsperity.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Code, PasswordInput, Select, Text } from '@mantine/core'; 2 | import { AxiosResponse } from 'axios'; 3 | import { FormEvent, useCallback } from 'react'; 4 | import { AlgorithmSummary } from '../../models'; 5 | import { useStore } from '../../store'; 6 | import { useAsync } from '../../utils/async'; 7 | import { createAxios } from '../../utils/axios'; 8 | import { AlgorithmList } from './AlgorithmList'; 9 | import { ErrorAlert } from './ErrorAlert'; 10 | import { HomeCard } from './HomeCard'; 11 | 12 | export function LoadFromProsperity(): JSX.Element { 13 | const idToken = useStore(state => state.idToken); 14 | const setIdToken = useStore(state => state.setIdToken); 15 | 16 | const round = useStore(state => state.round); 17 | const setRound = useStore(state => state.setRound); 18 | 19 | const loadAlgorithms = useAsync(async (): Promise => { 20 | const axios = createAxios(); 21 | 22 | let response: AxiosResponse; 23 | try { 24 | response = await axios.get( 25 | `https://bz97lt8b1e.execute-api.eu-west-1.amazonaws.com/prod/submission/algo/${round}`, 26 | ); 27 | } catch (err: any) { 28 | if (err.response?.status === 401) { 29 | throw new Error('ID token is invalid, please change it.'); 30 | } 31 | 32 | throw err; 33 | } 34 | 35 | return JSON.parse(response.data).sort( 36 | (a: AlgorithmSummary, b: AlgorithmSummary) => Date.parse(b.timestamp) - Date.parse(a.timestamp), 37 | ); 38 | }); 39 | 40 | const onSubmit = useCallback( 41 | (event?: FormEvent) => { 42 | event?.preventDefault(); 43 | 44 | if (idToken.trim().length > 0) { 45 | loadAlgorithms.call(); 46 | } 47 | }, 48 | [loadAlgorithms], 49 | ); 50 | 51 | return ( 52 | 53 | {/* prettier-ignore */} 54 | 55 | Requires your Prosperity ID token that is stored in the CognitoIdentityServiceProvider.<some id>.<email>.idToken cookie and/or in the CognitoIdentityServiceProvider.<some id>.<some id>.idToken local storage item on the Prosperity website. 56 | The ID token is remembered locally for ease-of-use but only valid for a limited amount of time, so you'll need to update this field often. 57 | 58 | 59 | 60 | Your ID token is only used to list your algorithms and to download algorithm logs and results. This website 61 | communicates directly with the API used by the Prosperity website and never sends data to other servers. The ID 62 | token is cached in your browser's local storage and not accessible by other websites. 63 | 64 | 65 | {loadAlgorithms.error && } 66 | 67 |
68 | setIdToken((e.target as HTMLInputElement).value)} 73 | mt="xs" 74 | /> 75 | 76 |