'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 ( {trades.map((trade, index) => { const height = Math.max(2, (Math.abs(trade.pnl) / maxAbsPnl) * 48); const isWin = trade.pnl > 0; const y = isWin ? centerY - height : centerY; 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 ( {bins.map((bin, index) => { const height = Math.max(2, (bin.count / maxCount) * 92); const x = index * barWidth + 2; const y = CHART_HEIGHT - height - CHART_PADDING; const midpoint = (bin.min + bin.max) / 2; 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 ); })} {hours.map((hour) => { const barWidth = CHART_WIDTH / 24; const height = Math.max(1, (Math.abs(hour.pnl) / maxHourAbsPnl) * 46); const isPositive = hour.pnl >= 0; const y = isPositive ? CHART_HEIGHT / 2 - height : CHART_HEIGHT / 2; return ( 0 ? 0.95 : 0.2} /> ); })} UTC hours, sessions: Asia 00-07, Europe 08-15, US 16-23 ); };