'use client';
import type { ReactNode } from 'react';
import { Box, Flex, SimpleGrid, Text } from '@chakra-ui/react';
import {
formatInteger,
formatPercent,
formatSignedNumber,
getPnlColor,
} from '#components/Shared/OrdersDrawer';
import type {
DistributionBin,
DrawdownPoint,
HourlyPnlStat,
RollingPerformancePoint,
SessionPnlStat,
StrategyTradePoint,
} from '#app/lib/strategyPerformance';
const CHART_WIDTH = 640;
const CHART_HEIGHT = 120;
const CHART_PADDING = 10;
const POSITIVE_CHART_COLOR = '#5eead4';
const NEGATIVE_CHART_COLOR = '#f87171';
const NEUTRAL_CHART_COLOR = '#6b7280';
const getChartPnlColor = (value: number) =>
value > 0
? POSITIVE_CHART_COLOR
: value < 0
? NEGATIVE_CHART_COLOR
: NEUTRAL_CHART_COLOR;
const buildPolylinePoints = (values: number[]) => {
if (!values.length) {
return '';
}
const min = Math.min(...values);
const max = Math.max(...values);
const range = max - min || 1;
const drawableWidth = CHART_WIDTH - CHART_PADDING * 2;
const drawableHeight = CHART_HEIGHT - CHART_PADDING * 2;
return values
.map((value, index) => {
const x =
CHART_PADDING +
(values.length === 1
? 0
: (index / (values.length - 1)) * drawableWidth);
const y = CHART_PADDING + ((max - value) / range) * drawableHeight;
return `${x.toFixed(2)},${y.toFixed(2)}`;
})
.join(' ');
};
export const ChartPanel = ({
title,
subtitle,
children,
}: {
title: string;
subtitle?: string;
children: ReactNode;
}) => (
{title}
{subtitle ? (
{subtitle}
) : null}
{children}
);
const EmptyChart = () => (
No trade data
);
export const DrawdownTimelineChart = ({
points,
}: {
points: DrawdownPoint[];
}) => {
if (points.length < 2) {
return ;
}
const values = points.map((point) => -point.drawdownPercent);
const maxDrawdown = Math.max(...points.map((point) => point.drawdownPercent));
const linePoints = buildPolylinePoints(values);
return (
max drawdown
{formatPercent(maxDrawdown)}
);
};
export const WinLossStreakTimelineChart = ({
trades,
}: {
trades: StrategyTradePoint[];
}) => {
if (!trades.length) {
return ;
}
const maxAbsPnl = Math.max(...trades.map((trade) => Math.abs(trade.pnl)), 1);
const centerY = CHART_HEIGHT / 2;
const barWidth = CHART_WIDTH / trades.length;
return (
wins above line, losses below
{formatInteger(trades.length)} trades
);
};
export const PnlDistributionChart = ({ bins }: { bins: DistributionBin[] }) => {
if (!bins.length) {
return ;
}
const maxCount = Math.max(...bins.map((bin) => bin.count), 1);
const barWidth = CHART_WIDTH / bins.length;
return (
trade P&L buckets
{formatInteger(bins.reduce((sum, bin) => sum + bin.count, 0))}
);
};
export const RollingPerformanceChart = ({
points,
}: {
points: RollingPerformancePoint[];
}) => {
if (points.length < 2) {
return ;
}
const winRateLine = buildPolylinePoints(points.map((point) => point.winRate));
const pnlLine = buildPolylinePoints(points.map((point) => point.pnl));
const latest = points[points.length - 1];
return (
teal P&L, yellow win rate
{formatPercent(latest?.winRate)} /{' '}
{formatSignedNumber(latest?.pnl ?? null)}
);
};
export const TimeOfDaySessionChart = ({
sessions,
hours,
}: {
sessions: SessionPnlStat[];
hours: HourlyPnlStat[];
}) => {
if (!sessions.some((session) => session.orders > 0)) {
return ;
}
const maxSessionAbsPnl = Math.max(
...sessions.map((session) => Math.abs(session.pnl)),
1,
);
const maxHourAbsPnl = Math.max(...hours.map((hour) => Math.abs(hour.pnl)), 1);
return (
{sessions.map((session) => {
const width = Math.max(
4,
(Math.abs(session.pnl) / maxSessionAbsPnl) * 100,
);
return (
{session.session}
{formatSignedNumber(session.pnl)}
{formatInteger(session.orders)} orders
);
})}
UTC hours, sessions: Asia 00-07, Europe 08-15, US 16-23
);
};