├── 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 | {column}
20 | ))}
21 |
22 |
23 | {rows}
24 |
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 | [](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 | 
10 |
11 | 
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 | Bid volume
68 | Price
69 | Ask volume
70 |
71 |
72 | {rows}
73 |
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 |
69 | Download logs
70 |
71 |
72 | Download results
73 |
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 |
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 |
95 |
96 | {loadAlgorithms.success && }
97 |
98 | );
99 | }
100 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "imc-prosperity-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": "yarn lint:eslint && yarn lint:prettier",
11 | "lint:eslint": "eslint --format codeframe 'src/**/*.ts' 'src/**/*.tsx' vite.config.ts",
12 | "lint:prettier": "prettier --check --ignore-path .gitignore '**/*.{ts,tsx,html,yml,json}'",
13 | "fix": "yarn fix:eslint && yarn fix:prettier",
14 | "fix:eslint": "yarn lint:eslint --fix",
15 | "fix:prettier": "prettier --write --ignore-path .gitignore '**/*.{ts,tsx,html,yml,json}'"
16 | },
17 | "dependencies": {
18 | "@emotion/react": "^11.10.6",
19 | "@mantine/core": "^6.0.2",
20 | "@mantine/dropzone": "^6.0.4",
21 | "@mantine/hooks": "^6.0.2",
22 | "@mantine/notifications": "^6.0.2",
23 | "@mantine/prism": "^6.0.2",
24 | "@tabler/icons-react": "^2.11.0",
25 | "apexcharts": "^3.37.1",
26 | "axios": "^1.3.4",
27 | "date-fns": "^2.29.3",
28 | "highcharts": "^10.3.3",
29 | "highcharts-react-official": "^3.2.0",
30 | "lodash": "^4.17.21",
31 | "react": "^18.2.0",
32 | "react-apexcharts": "^1.4.0",
33 | "react-dom": "^18.2.0",
34 | "react-router-dom": "^6.9.0",
35 | "zustand": "^4.3.6"
36 | },
37 | "devDependencies": {
38 | "@types/lodash": "^4.14.191",
39 | "@types/react": "^18.0.28",
40 | "@types/react-dom": "^18.0.11",
41 | "@typescript-eslint/eslint-plugin": "^5.56.0",
42 | "@typescript-eslint/parser": "^5.56.0",
43 | "@vitejs/plugin-react": "^3.1.0",
44 | "eslint": "^8.36.0",
45 | "eslint-config-prettier": "^8.8.0",
46 | "eslint-formatter-codeframe": "^7.32.1",
47 | "eslint-plugin-import": "^2.27.5",
48 | "eslint-plugin-react": "^7.32.2",
49 | "eslint-plugin-react-hooks": "^4.6.0",
50 | "husky": "4.3.8",
51 | "lint-staged": "^13.2.0",
52 | "prettier": "^2.8.5",
53 | "typescript": "^5.0.2",
54 | "vite": "^4.2.1"
55 | },
56 | "eslintConfig": {
57 | "root": true,
58 | "extends": [
59 | "eslint:recommended",
60 | "plugin:@typescript-eslint/recommended",
61 | "plugin:react/recommended",
62 | "plugin:react-hooks/recommended",
63 | "plugin:import/recommended",
64 | "plugin:import/typescript",
65 | "prettier"
66 | ],
67 | "plugins": [
68 | "@typescript-eslint"
69 | ],
70 | "parser": "@typescript-eslint/parser",
71 | "env": {
72 | "browser": true,
73 | "node": true
74 | },
75 | "rules": {
76 | "@typescript-eslint/no-explicit-any": "off",
77 | "@typescript-eslint/explicit-member-accessibility": "error",
78 | "@typescript-eslint/no-inferrable-types": "off",
79 | "@typescript-eslint/no-var-requires": "off",
80 | "@typescript-eslint/no-non-null-assertion": "off",
81 | "@typescript-eslint/explicit-function-return-type": [
82 | "error",
83 | {
84 | "allowExpressions": true
85 | }
86 | ],
87 | "react/react-in-jsx-scope": "off",
88 | "react-hooks/exhaustive-deps": "off",
89 | "import/order": [
90 | "error",
91 | {
92 | "alphabetize": {
93 | "order": "asc",
94 | "caseInsensitive": true
95 | }
96 | }
97 | ],
98 | "no-constant-condition": [
99 | "error",
100 | {
101 | "checkLoops": false
102 | }
103 | ]
104 | }
105 | },
106 | "husky": {
107 | "hooks": {
108 | "pre-commit": "lint-staged --concurrent false"
109 | }
110 | },
111 | "lint-staged": {
112 | "*.{ts,tsx}": [
113 | "eslint --format codeframe --fix"
114 | ],
115 | "*.{ts,tsx,html,yml,json}": [
116 | "prettier --write"
117 | ]
118 | },
119 | "prettier": {
120 | "singleQuote": true,
121 | "trailingComma": "all",
122 | "printWidth": 120,
123 | "arrowParens": "avoid"
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/src/pages/home/AlgorithmDetail.tsx:
--------------------------------------------------------------------------------
1 | import { Accordion, Button, Group, MantineColor, Text } from '@mantine/core';
2 | import { Prism } from '@mantine/prism';
3 | import { format } from 'date-fns';
4 | import { useNavigate } from 'react-router-dom';
5 | import { AlgorithmSummary } from '../../models';
6 | import { useStore } from '../../store';
7 | import { downloadAlgorithmResults, parseAlgorithmLogs } from '../../utils/algorithm';
8 | import { useAsync } from '../../utils/async';
9 | import { createAxios } from '../../utils/axios';
10 | import { PrismScrollArea } from '../base/PrismScrollArea';
11 | import { ErrorAlert } from './ErrorAlert';
12 |
13 | export interface AlgorithmDetailProps {
14 | position: number;
15 | algorithm: AlgorithmSummary;
16 | }
17 |
18 | export function AlgorithmDetail({ position, algorithm }: AlgorithmDetailProps): JSX.Element {
19 | const setAlgorithm = useStore(state => state.setAlgorithm);
20 |
21 | const navigate = useNavigate();
22 |
23 | const timestamp = format(Date.parse(algorithm.timestamp), 'yyyy-MM-dd HH:mm:ss');
24 |
25 | let statusColor: MantineColor = 'primary';
26 | switch (algorithm.status) {
27 | case 'FINISHED':
28 | statusColor = 'green';
29 | break;
30 | case 'ERROR':
31 | statusColor = 'red';
32 | break;
33 | }
34 |
35 | const downloadResults = useAsync(async () => {
36 | await downloadAlgorithmResults(algorithm.id);
37 | });
38 |
39 | const openInVisualizer = useAsync(async () => {
40 | const axios = createAxios();
41 |
42 | const logsResponse = await axios.get(
43 | `https://bz97lt8b1e.execute-api.eu-west-1.amazonaws.com/prod/submission/logs/${algorithm.id}`,
44 | );
45 |
46 | setAlgorithm(parseAlgorithmLogs(logsResponse.data, algorithm));
47 | navigate('/visualizer');
48 | });
49 |
50 | return (
51 |
52 |
53 | {/* prettier-ignore */}
54 |
55 | {position}. {algorithm.fileName} submitted at {timestamp} ({algorithm.status}) {algorithm.selectedForRound ? ' (active)' : ''}
56 |
57 |
58 |
59 | {openInVisualizer.error && }
60 |
61 |
69 | Download logs
70 |
71 |
72 | Download results
73 |
74 | {algorithm.status === 'FINISHED' && (
75 |
76 | Open in visualizer
77 |
78 | )}
79 |
80 |
81 | Id: {algorithm.id}
82 |
83 |
84 | File name: {algorithm.fileName}
85 |
86 |
87 | Submitted at: {timestamp}
88 |
89 |
90 | Submitted by: {algorithm.user.firstName} {algorithm.user.lastName}
91 |
92 |
93 | Status: {algorithm.status}
94 |
95 |
96 | Round: {algorithm.round}
97 |
98 |
99 | Selected for round: {algorithm.selectedForRound ? 'Yes' : 'No'}
100 |
101 |
102 | Content:
103 |
104 |
105 | {algorithm.content}
106 |
107 |
108 |
109 | );
110 | }
111 |
--------------------------------------------------------------------------------
/src/pages/home/HomePage.tsx:
--------------------------------------------------------------------------------
1 | import { Alert, Code, Container, Stack, Text } from '@mantine/core';
2 | import { Prism } from '@mantine/prism';
3 | import { PrismScrollArea } from '../base/PrismScrollArea';
4 | import { HomeCard } from './HomeCard';
5 | import { LoadFromElsewhere } from './LoadFromElsewhere';
6 | import { LoadFromFile } from './LoadFromFile';
7 | import { LoadFromProsperity } from './LoadFromProsperity';
8 |
9 | export function HomePage(): JSX.Element {
10 | const exampleCode = `
11 | import json
12 | from datamodel import Order, ProsperityEncoder, Symbol, Trade, TradingState
13 | from typing import Any
14 |
15 | class Logger:
16 | def __init__(self) -> None:
17 | self.logs = ""
18 |
19 | def print(self, *objects: Any, sep: str = " ", end: str = "\\n") -> None:
20 | self.logs += sep.join(map(str, objects)) + end
21 |
22 | def flush(self, state: TradingState, orders: dict[Symbol, list[Order]]) -> None:
23 | print(json.dumps({
24 | "state": self.compress_state(state),
25 | "orders": self.compress_orders(orders),
26 | "logs": self.logs,
27 | }, cls=ProsperityEncoder, separators=(",", ":"), sort_keys=True))
28 |
29 | self.logs = ""
30 |
31 | def compress_state(self, state: TradingState) -> dict[str, Any]:
32 | listings = []
33 | for listing in state.listings.values():
34 | listings.append([listing["symbol"], listing["product"], listing["denomination"]])
35 |
36 | order_depths = {}
37 | for symbol, order_depth in state.order_depths.items():
38 | order_depths[symbol] = [order_depth.buy_orders, order_depth.sell_orders]
39 |
40 | return {
41 | "t": state.timestamp,
42 | "l": listings,
43 | "od": order_depths,
44 | "ot": self.compress_trades(state.own_trades),
45 | "mt": self.compress_trades(state.market_trades),
46 | "p": state.position,
47 | "o": state.observations,
48 | }
49 |
50 | def compress_trades(self, trades: dict[Symbol, list[Trade]]) -> list[list[Any]]:
51 | compressed = []
52 | for arr in trades.values():
53 | for trade in arr:
54 | compressed.append([
55 | trade.symbol,
56 | trade.buyer,
57 | trade.seller,
58 | trade.price,
59 | trade.quantity,
60 | trade.timestamp,
61 | ])
62 |
63 | return compressed
64 |
65 | def compress_orders(self, orders: dict[Symbol, list[Order]]) -> list[list[Any]]:
66 | compressed = []
67 | for arr in orders.values():
68 | for order in arr:
69 | compressed.append([order.symbol, order.price, order.quantity])
70 |
71 | return compressed
72 |
73 | logger = Logger()
74 |
75 | class Trader:
76 | def run(self, state: TradingState) -> dict[Symbol, list[Order]]:
77 | orders = {}
78 |
79 | # TODO: Add logic
80 |
81 | logger.flush(state, orders)
82 | return orders
83 | `.trim();
84 |
85 | return (
86 |
87 |
88 |
89 | {/* prettier-ignore */}
90 |
91 | IMC Prosperity Visualizer is a visualizer for IMC Prosperity 1 algorithms.
92 | Its source code is available in the jmerle/imc-prosperity-visualizer GitHub repository.
93 | Load an algorithm below to get started.
94 |
95 |
96 | {/* prettier-ignore */}
97 |
98 | Please note that this visualizer only supports IMC Prosperity 1 algorithms.
99 | See the IMC Prosperity 2 Visualizer by the same author for IMC Prosperity 2 algorithms.
100 |
101 |
102 |
103 |
104 |
105 | IMC Prosperity Visualizer assumes your algorithm logs in a certain format. Algorithms that use a different
106 | logging format may cause unexpected errors when opening them in the visualizer. Please use the following
107 | boilerplate for your algorithm (or adapt your algorithm to use the logger from this code) and use{' '}
108 | logger.print() where you would normally use print():
109 |
110 | {exampleCode}
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 | );
121 | }
122 |
--------------------------------------------------------------------------------
/src/pages/visualizer/Chart.tsx:
--------------------------------------------------------------------------------
1 | import { useMantineTheme } from '@mantine/core';
2 | import Highcharts from 'highcharts/highstock';
3 | import HighchartsAccessibility from 'highcharts/modules/accessibility';
4 | import HighchartsExporting from 'highcharts/modules/exporting';
5 | import HighchartsOfflineExporting from 'highcharts/modules/offline-exporting';
6 | import HighchartsHighContrastDarkTheme from 'highcharts/themes/high-contrast-dark';
7 | import HighchartsReact from 'highcharts-react-official';
8 | import merge from 'lodash/merge';
9 | import { useMemo } from 'react';
10 | import { formatNumber } from '../../utils/format';
11 | import { VisualizerCard } from './VisualizerCard';
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 | };
36 |
37 | theme(highchartsMock as any);
38 |
39 | return highchartsMock._modules['Core/Globals.js'].theme! as Highcharts.Options;
40 | }
41 |
42 | interface ChartProps {
43 | title: string;
44 | series: Highcharts.SeriesOptionsType[];
45 | min?: number;
46 | max?: number;
47 | }
48 |
49 | export function Chart({ title, series, min, max }: ChartProps): JSX.Element {
50 | const theme = useMantineTheme();
51 |
52 | const options = useMemo((): Highcharts.Options => {
53 | const themeOptions = theme.colorScheme === 'light' ? {} : getThemeOptions(HighchartsHighContrastDarkTheme);
54 |
55 | const chartOptions: Highcharts.Options = {
56 | chart: {
57 | animation: false,
58 | height: 400,
59 | zooming: {
60 | type: 'x',
61 | },
62 | panning: {
63 | enabled: true,
64 | type: 'x',
65 | },
66 | panKey: 'shift',
67 | numberFormatter: formatNumber,
68 | events: {
69 | load() {
70 | Highcharts.addEvent(this.tooltip, 'headerFormatter', (e: any) => {
71 | if (e.isFooter) {
72 | return true;
73 | }
74 |
75 | let timestamp = e.labelConfig.point.x;
76 |
77 | if (e.labelConfig.point.dataGroup) {
78 | const xData = e.labelConfig.series.xData;
79 | const lastTimestamp = xData[xData.length - 1];
80 | if (timestamp + 100 * e.labelConfig.point.dataGroup.length >= lastTimestamp) {
81 | timestamp = lastTimestamp;
82 | }
83 | }
84 |
85 | e.text = `Timestamp ${formatNumber(timestamp)} `;
86 | return false;
87 | });
88 | },
89 | },
90 | },
91 | title: {
92 | text: title,
93 | },
94 | credits: {
95 | href: 'javascript:window.open("https://www.highcharts.com/?credits", "_blank")',
96 | },
97 | plotOptions: {
98 | series: {
99 | dataGrouping: {
100 | approximation(this: any, values: number[]): number {
101 | const endIndex = this.dataGroupInfo.start + this.dataGroupInfo.length;
102 | if (endIndex < this.xData.length) {
103 | return values[0];
104 | } else {
105 | return values[values.length - 1];
106 | }
107 | },
108 | anchor: 'start',
109 | firstAnchor: 'firstPoint',
110 | lastAnchor: 'lastPoint',
111 | units: [['second', [1, 2, 5, 10]]],
112 | },
113 | },
114 | },
115 | xAxis: {
116 | type: 'datetime',
117 | title: {
118 | text: 'Timestamp',
119 | },
120 | crosshair: {
121 | width: 1,
122 | },
123 | labels: {
124 | formatter: params => formatNumber(params.value as number),
125 | },
126 | },
127 | yAxis: {
128 | opposite: false,
129 | allowDecimals: false,
130 | min,
131 | max,
132 | },
133 | tooltip: {
134 | split: false,
135 | shared: true,
136 | outside: true,
137 | },
138 | legend: {
139 | enabled: true,
140 | },
141 | rangeSelector: {
142 | enabled: false,
143 | },
144 | navigator: {
145 | enabled: false,
146 | },
147 | scrollbar: {
148 | enabled: false,
149 | },
150 | series,
151 | };
152 |
153 | return merge(themeOptions, chartOptions);
154 | }, [theme, title, series, min, max]);
155 |
156 | return (
157 |
158 |
159 |
160 | );
161 | }
162 |
--------------------------------------------------------------------------------
/src/pages/base/Page.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Box,
3 | Burger,
4 | Center,
5 | Container,
6 | createStyles,
7 | Group,
8 | Header,
9 | Paper,
10 | Text,
11 | Tooltip,
12 | Transition,
13 | useMantineTheme,
14 | } from '@mantine/core';
15 | import { useToggle } from '@mantine/hooks';
16 | import { IconEye } from '@tabler/icons-react';
17 | import { useCallback } from 'react';
18 | import { Link, Outlet, useLocation } from 'react-router-dom';
19 | import { useStore } from '../../store';
20 | import { ScrollToTop } from './ScrollToTop';
21 | import { ThemeSwitch } from './ThemeSwitch';
22 |
23 | const HEADER_HEIGHT = 56;
24 |
25 | const useStyles = createStyles(theme => ({
26 | header: {
27 | backgroundColor: theme.colors[theme.primaryColor][6],
28 | borderBottom: 'none',
29 | position: 'relative',
30 | zIndex: 1,
31 | marginBottom: theme.spacing.md,
32 | },
33 |
34 | container: {
35 | display: 'flex',
36 | justifyContent: 'space-between',
37 | alignItems: 'center',
38 | height: '100%',
39 | },
40 |
41 | title: {
42 | color: theme.white,
43 | textDecoration: 'none',
44 | },
45 |
46 | icon: {
47 | verticalAlign: 'top',
48 | paddingRight: '2px',
49 | },
50 |
51 | burger: {
52 | [theme.fn.largerThan('sm')]: {
53 | display: 'none',
54 | },
55 | },
56 |
57 | dropdown: {
58 | position: 'absolute',
59 | top: HEADER_HEIGHT,
60 | left: 0,
61 | right: 0,
62 | zIndex: 0,
63 | borderTopRightRadius: 0,
64 | borderTopLeftRadius: 0,
65 | borderTopWidth: 0,
66 | overflow: 'hidden',
67 |
68 | [theme.fn.largerThan('sm')]: {
69 | display: 'none',
70 | },
71 | },
72 |
73 | links: {
74 | [theme.fn.smallerThan('sm')]: {
75 | display: 'none',
76 | },
77 | },
78 |
79 | link: {
80 | display: 'block',
81 | lineHeight: 1,
82 | padding: '8px 12px',
83 | borderRadius: theme.radius.sm,
84 | textDecoration: 'none',
85 | color: theme.white,
86 | fontSize: theme.fontSizes.sm,
87 | fontWeight: 500,
88 |
89 | '&:hover': {
90 | backgroundColor: theme.fn.rgba(theme.colors[theme.primaryColor][9], 0.5),
91 | },
92 |
93 | [theme.fn.smallerThan('sm')]: {
94 | borderRadius: 0,
95 | padding: theme.spacing.md,
96 | color: theme.colors.gray[7],
97 |
98 | '&:hover': {
99 | backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[6] : theme.colors.gray[0],
100 | },
101 | },
102 | },
103 |
104 | linkActive: {
105 | '&, &:hover': {
106 | backgroundColor: theme.colors[theme.primaryColor][9],
107 | color: theme.white,
108 |
109 | [theme.fn.smallerThan('sm')]: {
110 | backgroundColor: theme.fn.variant({ variant: 'light', color: theme.primaryColor }).background,
111 | color: theme.fn.variant({ variant: 'light', color: theme.primaryColor }).color,
112 | },
113 | },
114 | },
115 |
116 | linkDisabled: {
117 | cursor: 'not-allowed',
118 | },
119 |
120 | tooltip: {
121 | [theme.fn.smallerThan('sm')]: {
122 | width: '100%',
123 | },
124 | },
125 | }));
126 |
127 | export function Page(): JSX.Element {
128 | const { classes, cx } = useStyles();
129 | const theme = useMantineTheme();
130 |
131 | const [burgerOpened, toggleBurgerOpened] = useToggle();
132 | const location = useLocation();
133 |
134 | const algorithm = useStore(state => state.algorithm);
135 |
136 | const closeBurger = useCallback(() => toggleBurgerOpened(false), []);
137 |
138 | const links = [
139 |
145 | Home
146 | ,
147 | ];
148 |
149 | if (algorithm !== null) {
150 | links.push(
151 |
157 | Visualizer
158 | ,
159 | );
160 | } else {
161 | links.push(
162 |
163 | Visualizer
164 | ,
165 | );
166 | }
167 |
168 | return (
169 | <>
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 | IMC Prosperity Visualizer
179 |
180 |
181 |
182 |
183 |
184 | {links}
185 |
186 |
187 | toggleBurgerOpened()}
190 | color={theme.white}
191 | className={classes.burger}
192 | size="sm"
193 | />
194 |
195 |
196 | {styles => (
197 |
198 | {links}
199 |
200 | )}
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 | >
213 | );
214 | }
215 |
--------------------------------------------------------------------------------
/src/utils/algorithm.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ActivityLogRow,
3 | Algorithm,
4 | AlgorithmSummary,
5 | CompressedOrder,
6 | CompressedSandboxLogRow,
7 | CompressedTrade,
8 | CompressedTradingState,
9 | Listing,
10 | Order,
11 | OrderDepth,
12 | ProsperitySymbol,
13 | SandboxLogRow,
14 | Trade,
15 | TradingState,
16 | } from '../models';
17 | import { createAxios } from './axios';
18 |
19 | export async function downloadAlgorithmResults(algorithmId: string): Promise {
20 | const axios = createAxios();
21 |
22 | const detailsResponse = await axios.get(
23 | `https://bz97lt8b1e.execute-api.eu-west-1.amazonaws.com/prod/results/tutorial/${algorithmId}`,
24 | );
25 |
26 | const resultsUrl = JSON.parse(detailsResponse.data).algo.summary.activitiesLog;
27 |
28 | const link = document.createElement('a');
29 | link.href = resultsUrl;
30 | link.download = 'results.csv';
31 | link.target = '_blank';
32 | link.rel = 'noreferrer';
33 |
34 | document.body.appendChild(link);
35 | link.click();
36 | link.remove();
37 | }
38 |
39 | function getColumnValues(columns: string[], indices: number[]): number[] {
40 | const values: number[] = [];
41 |
42 | for (const index of indices) {
43 | const value = columns[index];
44 | if (value !== '') {
45 | values.push(Number(value));
46 | }
47 | }
48 |
49 | return values;
50 | }
51 |
52 | function getActivityLogs(logLines: string[]): ActivityLogRow[] {
53 | const headerIndex = logLines.indexOf('Activities log:');
54 | if (headerIndex === -1) {
55 | return [];
56 | }
57 |
58 | const rows: ActivityLogRow[] = [];
59 |
60 | for (let i = headerIndex + 2; i < logLines.length; i++) {
61 | const columns = logLines[i].split(';');
62 | rows.push({
63 | day: Number(columns[0]),
64 | timestamp: Number(columns[1]),
65 | product: columns[2],
66 | bidPrices: getColumnValues(columns, [3, 5, 7]),
67 | bidVolumes: getColumnValues(columns, [4, 6, 8]),
68 | askPrices: getColumnValues(columns, [9, 11, 13]),
69 | askVolumes: getColumnValues(columns, [10, 12, 14]),
70 | midPrice: Number(columns[15]),
71 | profitLoss: Number(columns[16]),
72 | });
73 | }
74 |
75 | return rows;
76 | }
77 |
78 | function decompressTrades(compressed: CompressedTrade[]): Record {
79 | const trades: Record = {};
80 |
81 | for (const trade of compressed) {
82 | if (trades[trade[0]] === undefined) {
83 | trades[trade[0]] = [];
84 | }
85 |
86 | trades[trade[0]].push({
87 | symbol: trade[0],
88 | buyer: trade[1],
89 | seller: trade[2],
90 | price: trade[3],
91 | quantity: trade[4],
92 | timestamp: trade[5],
93 | });
94 | }
95 |
96 | return trades;
97 | }
98 |
99 | function decompressState(compressed: CompressedTradingState): TradingState {
100 | const listings: Record = {};
101 | for (const listing of compressed.l) {
102 | listings[listing[0]] = {
103 | symbol: listing[0],
104 | product: listing[1],
105 | denomination: listing[2],
106 | };
107 | }
108 |
109 | const order_depths: Record = {};
110 | for (const symbol of Object.keys(compressed.od)) {
111 | order_depths[symbol] = {
112 | buy_orders: compressed.od[symbol][0],
113 | sell_orders: compressed.od[symbol][1],
114 | };
115 | }
116 |
117 | return {
118 | timestamp: compressed.t,
119 | listings,
120 | order_depths,
121 | own_trades: decompressTrades(compressed.ot),
122 | market_trades: decompressTrades(compressed.mt),
123 | position: compressed.p,
124 | observations: compressed.o,
125 | };
126 | }
127 |
128 | function decompressOrders(compressed: CompressedOrder[]): Record {
129 | const orders: Record = {};
130 |
131 | for (const order of compressed) {
132 | if (orders[order[0]] === undefined) {
133 | orders[order[0]] = [];
134 | }
135 |
136 | orders[order[0]].push({
137 | symbol: order[0],
138 | price: order[1],
139 | quantity: order[2],
140 | });
141 | }
142 |
143 | return orders;
144 | }
145 |
146 | function decompressSandboxLogRow(compressed: CompressedSandboxLogRow): SandboxLogRow {
147 | return {
148 | state: decompressState(compressed.state),
149 | orders: decompressOrders(compressed.orders),
150 | logs: compressed.logs,
151 | };
152 | }
153 |
154 | function getSandboxLogs(logLines: string[]): SandboxLogRow[] {
155 | const headerIndex = logLines.indexOf('Sandbox logs:');
156 | if (headerIndex === -1) {
157 | return [];
158 | }
159 |
160 | const rows: SandboxLogRow[] = [];
161 | for (let i = headerIndex + 1; i < logLines.length; i++) {
162 | const line = logLines[i];
163 | if (line.endsWith(':')) {
164 | break;
165 | }
166 |
167 | let unparsed: string;
168 | if (line.startsWith('{')) {
169 | unparsed = line;
170 | } else {
171 | if (line.length === 0 || line.endsWith(' ') || !/\d/.test(line[0])) {
172 | continue;
173 | }
174 |
175 | unparsed = line.substring(line.indexOf(' ') + 1);
176 | }
177 |
178 | if (!unparsed.startsWith('{"logs":"')) {
179 | continue;
180 | }
181 |
182 | try {
183 | const parsed = JSON.parse(unparsed);
184 |
185 | let row: SandboxLogRow;
186 | if (parsed.state.t !== undefined) {
187 | row = decompressSandboxLogRow(parsed);
188 | } else {
189 | row = parsed;
190 | }
191 |
192 | rows.push(row);
193 | } catch (err) {
194 | console.error(err);
195 | throw new Error('Sandbox logs are in invalid format, please see the prerequisites section above.');
196 | }
197 | }
198 |
199 | return rows;
200 | }
201 |
202 | function getSubmissionLogs(logLines: string[]): string {
203 | const headerIndex = logLines.indexOf('Submission logs:');
204 | if (headerIndex === -1) {
205 | return '';
206 | }
207 |
208 | const lines = [];
209 | for (let i = headerIndex + 1; i < logLines.length; i++) {
210 | if (logLines[i].endsWith(':')) {
211 | break;
212 | }
213 |
214 | lines.push(logLines[i]);
215 | }
216 |
217 | return lines.join('\n').trimEnd();
218 | }
219 |
220 | export function parseAlgorithmLogs(logs: string, summary?: AlgorithmSummary): Algorithm {
221 | const logLines = logs.trim().split('\n');
222 |
223 | const activityLogs = getActivityLogs(logLines);
224 | const sandboxLogs = getSandboxLogs(logLines);
225 | const submissionLogs = getSubmissionLogs(logLines);
226 |
227 | if (activityLogs.length === 0 || sandboxLogs.length === 0) {
228 | throw new Error('Sandbox logs are in invalid format, please see the prerequisites section above.');
229 | }
230 |
231 | return {
232 | summary,
233 | activityLogs,
234 | sandboxLogs,
235 | submissionLogs,
236 | };
237 | }
238 |
--------------------------------------------------------------------------------