Files
evotraders/frontend/src/components/NetValueChart.jsx

831 lines
29 KiB
JavaScript

import React, { useMemo, useState, useEffect } from 'react';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts';
import { formatNumber, formatFullNumber } from '../utils/formatters';
/**
* Helper function to get the start time of the most recent trading session
* Trading session: 22:30 - next day 05:00
* @param {Date|null} virtualTime - Virtual time from server (for mock mode), or null to use real time
*/
function getRecentTradingSessionStart(virtualTime = null) {
// Use virtual time if provided (for mock mode), otherwise use real time
let now;
if (virtualTime) {
// Ensure virtualTime is a valid Date object
if (virtualTime instanceof Date && !isNaN(virtualTime.getTime())) {
now = virtualTime;
} else if (typeof virtualTime === 'string') {
now = new Date(virtualTime);
if (isNaN(now.getTime())) {
console.warn('Invalid virtualTime string, using current time:', virtualTime);
now = new Date();
}
} else {
console.warn('Invalid virtualTime type, using current time:', typeof virtualTime);
now = new Date();
}
} else {
now = new Date();
}
const currentHour = now.getHours();
const currentMinute = now.getMinutes();
// Check if currently in trading session
const isInTradingSession = (currentHour === 22 && currentMinute >= 30) ||
currentHour >= 23 ||
(currentHour >= 0 && currentHour < 5) ||
(currentHour === 5 && currentMinute === 0);
let sessionStartTime;
if (isInTradingSession) {
// Currently in trading session, find today's 22:30
sessionStartTime = new Date(now);
sessionStartTime.setHours(22, 30, 0, 0);
// If current time is before 22:30, it means yesterday's 22:30
if (now < sessionStartTime) {
sessionStartTime.setDate(sessionStartTime.getDate() - 1);
}
} else {
// Not in trading session, find previous session start (yesterday 22:30)
sessionStartTime = new Date(now);
sessionStartTime.setDate(sessionStartTime.getDate() - 1);
sessionStartTime.setHours(22, 30, 0, 0);
}
return sessionStartTime;
}
/**
* Helper function to filter strategy data for live view
* NOTE: Live mode returns are now pre-processed by the backend, restricted to the
* latest trading session and already starting at 0% at session start. This helper
* is kept for potential future use but is no longer used in live mode.
*/
function filterStrategyDataForLive(strategyData, equity, sessionStartTime) {
if (!strategyData || strategyData.length === 0 || !equity || equity.length === 0) return [];
try {
if (!sessionStartTime || isNaN(sessionStartTime.getTime())) {
console.warn('Invalid sessionStartTime in filterStrategyDataForLive');
return [];
}
const sessionStartTimestamp = sessionStartTime.getTime();
// Find the last index before session
let lastDataBeforeSession = null;
for (let i = equity.length - 1; i >= 0; i--) {
if (equity[i] && typeof equity[i].t === 'number' && equity[i].t < sessionStartTimestamp) {
if (strategyData[i] && strategyData[i].v !== undefined && strategyData[i].v !== null) {
lastDataBeforeSession = strategyData[i];
}
break;
}
}
// Find data points in the session
const sessionData = [];
for (let i = 0; i < equity.length; i++) {
if (equity[i] && typeof equity[i].t === 'number' &&
equity[i].t >= sessionStartTimestamp &&
strategyData[i] &&
strategyData[i].v !== undefined && strategyData[i].v !== null) {
sessionData.push(strategyData[i]);
}
}
// If we have a value before session and session data, add the start point
// Create a start point with timestamp just before session start
if (lastDataBeforeSession && sessionData.length > 0) {
const startPoint = {
t: sessionStartTimestamp - 1,
v: lastDataBeforeSession.v
};
return [startPoint, ...sessionData];
}
return sessionData;
} catch (error) {
console.error('Error in filterStrategyDataForLive:', error);
return [];
}
}
/**
* Net Value Chart Component
* Displays portfolio value over time with multiple strategy comparisons
*/
export default function NetValueChart({ equity, baseline, baseline_vw, momentum, strategies, equity_return, baseline_return, baseline_vw_return, momentum_return, chartTab = 'all', virtualTime = null }) {
const [activePoint, setActivePoint] = useState(null);
const [stableYRange, setStableYRange] = useState(null);
const [legendTooltip, setLegendTooltip] = useState(null);
// Legend descriptions
const legendDescriptions = {
'EvoTraders': 'EvoTraders is our agents investment strategy',
'Buy & Hold (EW)': 'Equal Weight: Can be viewed as an equal-weighted index of all invested stocks',
'Buy & Hold (VW)': 'Value Weighted: Can be viewed as a market-cap weighted index of all invested stocks',
'Momentum': 'Momentum Strategy: Buy stocks that have performed well in the past',
};
// For live mode, use cumulative returns calculated by backend
// For all mode, use portfolio values directly
const dataSource = useMemo(() => {
if (chartTab === 'live') {
return {
equity: equity_return || equity,
baseline: baseline_return || baseline,
baseline_vw: baseline_vw_return || baseline_vw,
momentum: momentum_return || momentum
};
}
return {
equity: equity,
baseline: baseline,
baseline_vw: baseline_vw,
momentum: momentum
};
}, [chartTab, equity, baseline, baseline_vw, momentum, equity_return, baseline_return, baseline_vw_return, momentum_return]);
// Filter equity data based on chartTab
const filteredEquity = useMemo(() => {
if (chartTab === 'all') {
const sourceEquity = dataSource.equity;
if (!sourceEquity || sourceEquity.length === 0) return [];
// ALL chart: Show only the last point per day
// Logic: Keep the last equity value before 22:30 each day (the last equity value before US next trading day opens)
// Data after 22:30 belongs to the next trading day's session and is not shown in this chart
// Time handling: timestamp(ms) -> UTC -> Asia/Shanghai timezone, then group and filter based on Asia/Shanghai time
const dailyData = {};
sourceEquity.forEach((d) => {
// Timestamp is in milliseconds, first create UTC time, then convert to Asia/Shanghai timezone
// Equivalent to: pd.to_datetime(timestamp, unit='ms', utc=True).dt.tz_convert('Asia/Shanghai')
const utcDate = new Date(d.t); // timestamp(ms) -> UTC time
// Use Intl API to get date/time components in Asia/Shanghai timezone
const formatter = new Intl.DateTimeFormat('en-US', {
timeZone: 'Asia/Shanghai',
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
hour12: false
});
const parts = formatter.formatToParts(utcDate);
const year = parts.find(p => p.type === 'year').value;
const month = parts.find(p => p.type === 'month').value;
const day = parts.find(p => p.type === 'day').value;
const hour = parseInt(parts.find(p => p.type === 'hour').value);
const minute = parseInt(parts.find(p => p.type === 'minute').value);
// Check if before 22:30 (Asia/Shanghai timezone)
const isBefore2230 = hour < 22 || (hour === 22 && minute < 30);
// Only process data before 22:30
if (isBefore2230) {
// Use Asia/Shanghai timezone date as key
const dateKey = `${year}-${month}-${day}`;
// Update if this day has no data yet, or if current data is later in time
if (!dailyData[dateKey] || new Date(d.t) > new Date(dailyData[dateKey].t)) {
dailyData[dateKey] = d;
}
}
});
// Convert to array and sort by time
return Object.values(dailyData).sort((a, b) => a.t - b.t);
} else if (chartTab === 'live') {
// LIVE chart: Show all updates from the most recent trading session (22:30-05:00)
// Live mode: Backend has already returned return curves for "current trading session + 0% starting point", frontend can use directly
const sourceEquity = dataSource.equity;
if (!sourceEquity || sourceEquity.length === 0) return [];
return sourceEquity;
}
return dataSource.equity || [];
}, [dataSource.equity, chartTab, virtualTime]);
// Helper function to get daily indices for 'all' view
const getDailyIndices = useMemo(() => {
if (!equity || equity.length === 0) return new Set();
const dailyIndices = new Set();
const dailyData = {};
const formatter = new Intl.DateTimeFormat('en-US', {
timeZone: 'Asia/Shanghai',
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
hour12: false
});
equity.forEach((d, idx) => {
const utcDate = new Date(d.t);
const parts = formatter.formatToParts(utcDate);
const hour = parseInt(parts.find(p => p.type === 'hour').value);
const minute = parseInt(parts.find(p => p.type === 'minute').value);
// Check if before 22:30 (Asia/Shanghai timezone)
const isBefore2230 = hour < 22 || (hour === 22 && minute < 30);
// Only process data before 22:30
if (isBefore2230) {
const year = parts.find(p => p.type === 'year').value;
const month = parts.find(p => p.type === 'month').value;
const day = parts.find(p => p.type === 'day').value;
const dateKey = `${year}-${month}-${day}`;
if (!dailyData[dateKey] || new Date(d.t) > new Date(dailyData[dateKey].t)) {
dailyData[dateKey] = { data: d, index: idx };
}
}
});
Object.values(dailyData).forEach(({ index }) => dailyIndices.add(index));
return dailyIndices;
}, [equity]);
// Filter baseline, baseline_vw, momentum, strategies to match filteredEquity indices
const filteredBaseline = useMemo(() => {
const sourceBaseline = dataSource.baseline;
if (!sourceBaseline || sourceBaseline.length === 0 || !equity || equity.length === 0) return [];
if (chartTab === 'all') {
return sourceBaseline.filter((_, idx) => getDailyIndices.has(idx));
} else if (chartTab === 'live') {
// Live mode: Use backend pre-processed baseline return curves directly
return sourceBaseline;
}
return sourceBaseline;
}, [dataSource.baseline, equity, chartTab, getDailyIndices, virtualTime]);
const filteredBaselineVw = useMemo(() => {
const sourceBaselineVw = dataSource.baseline_vw;
if (!sourceBaselineVw || sourceBaselineVw.length === 0 || !equity || equity.length === 0) return [];
if (chartTab === 'all') {
return sourceBaselineVw.filter((_, idx) => getDailyIndices.has(idx));
} else if (chartTab === 'live') {
// Live mode: Use backend pre-processed baseline return curves directly
return sourceBaselineVw;
}
return sourceBaselineVw;
}, [dataSource.baseline_vw, equity, chartTab, getDailyIndices, virtualTime]);
const filteredMomentum = useMemo(() => {
const sourceMomentum = dataSource.momentum;
if (!sourceMomentum || sourceMomentum.length === 0 || !equity || equity.length === 0) return [];
if (chartTab === 'all') {
return sourceMomentum.filter((_, idx) => getDailyIndices.has(idx));
} else if (chartTab === 'live') {
// Live mode: Use backend pre-processed momentum return curves directly
return sourceMomentum;
}
return sourceMomentum;
}, [dataSource.momentum, equity, chartTab, getDailyIndices, virtualTime]);
const filteredStrategies = useMemo(() => {
if (!strategies || strategies.length === 0 || !equity || equity.length === 0) return [];
if (chartTab === 'all') {
return strategies.filter((_, idx) => getDailyIndices.has(idx));
} else if (chartTab === 'live') {
const sessionStartTime = getRecentTradingSessionStart(virtualTime);
return filterStrategyDataForLive(strategies, equity, sessionStartTime);
}
return strategies;
}, [strategies, equity, chartTab, getDailyIndices, virtualTime]);
const chartData = useMemo(() => {
if (!filteredEquity || filteredEquity.length === 0) return [];
try {
// LIVE mode: Align all curves by timestamp with forward filling to ensure consistent point counts and aligned starting points
if (chartTab === 'live') {
// Build timestamp -> value mapping
const toMap = (arr) => {
const m = new Map();
if (Array.isArray(arr)) {
arr.forEach((p) => {
if (p && typeof p.t === 'number' && typeof p.v === 'number') {
m.set(p.t, p.v);
}
});
}
return m;
};
const portfolioMap = toMap(filteredEquity);
const baselineMap = toMap(filteredBaseline);
const baselineVwMap = toMap(filteredBaselineVw);
const momentumMap = toMap(filteredMomentum);
const strategyMap = toMap(filteredStrategies);
// Collect all timestamps, sort by time
const timestampSet = new Set();
[filteredEquity, filteredBaseline, filteredBaselineVw, filteredMomentum, filteredStrategies].forEach(arr => {
if (Array.isArray(arr)) {
arr.forEach(p => {
if (p && typeof p.t === 'number') timestampSet.add(p.t);
});
}
});
const timestamps = Array.from(timestampSet).sort((a, b) => a - b);
if (timestamps.length === 0) return [];
// Current values for forward filling, initialized to 0% to ensure starting point alignment
let currentPortfolio = 0;
let currentBaseline = 0;
let currentBaselineVw = 0;
let currentMomentum = 0;
let currentStrategy = 0;
return timestamps.map((t, idx) => {
if (portfolioMap.has(t)) currentPortfolio = portfolioMap.get(t);
if (baselineMap.has(t)) currentBaseline = baselineMap.get(t);
if (baselineVwMap.has(t)) currentBaselineVw = baselineVwMap.get(t);
if (momentumMap.has(t)) currentMomentum = momentumMap.get(t);
if (strategyMap.has(t)) currentStrategy = strategyMap.get(t);
const date = new Date(t);
if (isNaN(date.getTime())) {
console.warn('Invalid timestamp in live chart data:', t);
return null;
}
return {
index: idx,
time:
date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
}) +
' ' +
date.toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit',
hour12: false,
}),
timestamp: t,
portfolio: currentPortfolio,
baseline: currentBaseline,
baseline_vw: currentBaselineVw,
momentum: currentMomentum,
strategy: currentStrategy,
};
}).filter(item => item !== null);
}
// ALL mode: Keep the original index-based alignment logic
return filteredEquity.map((d, idx) => {
if (!d || typeof d.t !== 'number' || typeof d.v !== 'number') {
console.warn('Invalid equity data point:', d);
return null;
}
const date = new Date(d.t);
if (isNaN(date.getTime())) {
console.warn('Invalid timestamp:', d.t);
return null;
}
const baselineVal = filteredBaseline?.[idx]
? (typeof filteredBaseline[idx] === 'object' ? filteredBaseline[idx].v : filteredBaseline[idx])
: null;
const baselineVwVal = filteredBaselineVw?.[idx]
? (typeof filteredBaselineVw[idx] === 'object' ? filteredBaselineVw[idx].v : filteredBaselineVw[idx])
: null;
const momentumVal = filteredMomentum?.[idx]
? (typeof filteredMomentum[idx] === 'object' ? filteredMomentum[idx].v : filteredMomentum[idx])
: null;
const strategyVal = filteredStrategies?.[idx]
? (typeof filteredStrategies[idx] === 'object' ? filteredStrategies[idx].v : filteredStrategies[idx])
: null;
return {
index: idx,
time:
date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) +
' ' +
date.toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit',
hour12: false,
}),
timestamp: d.t,
portfolio: d.v,
baseline: baselineVal || null,
baseline_vw: baselineVwVal || null,
momentum: momentumVal || null,
strategy: strategyVal || null,
};
}).filter(item => item !== null); // Remove null entries
} catch (error) {
console.error('Error processing chart data:', error);
return [];
}
}, [filteredEquity, filteredBaseline, filteredBaselineVw, filteredMomentum, filteredStrategies, chartTab]);
const { yMin, yMax, xTickIndices } = useMemo(() => {
if (chartData.length === 0) return { yMin: 0, yMax: 1, xTickIndices: [] };
// Calculate min and max from all series
const allValues = chartData.flatMap(d =>
[d.portfolio, d.baseline, d.baseline_vw, d.momentum, d.strategy].filter(v => v !== null && isFinite(v))
);
if (allValues.length === 0) {
return { yMin: 0, yMax: 1000000, xTickIndices: [] };
}
const dataMin = Math.min(...allValues);
const dataMax = Math.max(...allValues);
const range = dataMax - dataMin || 1;
// For live mode (percentage data), use smaller padding and finer rounding
// For all mode (dollar amounts), use larger padding and coarser rounding
const isLiveMode = chartTab === 'live';
const paddingFactor = isLiveMode ? range * 0.15 : range * 0.03;
let yMinCalc = dataMin - paddingFactor;
let yMaxCalc = dataMax + paddingFactor;
// Smart rounding based on magnitude and mode
const magnitude = Math.max(Math.abs(yMinCalc), Math.abs(yMaxCalc));
let roundTo;
if (isLiveMode) {
// For percentage data, use much finer rounding
if (magnitude >= 100) {
roundTo = 10;
} else if (magnitude >= 10) {
roundTo = 1;
} else if (magnitude >= 1) {
roundTo = 0.1;
} else {
roundTo = 0.01;
}
} else {
// For dollar amounts, use coarser rounding
if (magnitude >= 1e6) {
roundTo = 10000;
} else if (magnitude >= 1e5) {
roundTo = 5000;
} else if (magnitude >= 1e4) {
roundTo = 1000;
} else {
roundTo = 100;
}
}
yMinCalc = Math.floor(yMinCalc / roundTo) * roundTo;
yMaxCalc = Math.ceil(yMaxCalc / roundTo) * roundTo;
// Stable range to prevent frequent updates
if (stableYRange) {
const { min: stableMin, max: stableMax } = stableYRange;
const stableRange = stableMax - stableMin;
const threshold = stableRange * 0.05;
const needsUpdate =
dataMin < (stableMin + threshold) ||
dataMax > (stableMax - threshold);
if (!needsUpdate) {
yMinCalc = stableMin;
yMaxCalc = stableMax;
}
}
// Calculate x-axis tick indices
const safeLength = Math.min(chartData.length, 10000);
const targetTicks = Math.min(8, Math.max(5, Math.floor(safeLength / 10)));
const step = Math.max(1, Math.floor(safeLength / (targetTicks - 1)));
const indices = [];
for (let i = 0; i < safeLength && indices.length < 100; i += step) {
indices.push(i);
}
if (safeLength > 0 && indices[indices.length - 1] !== safeLength - 1) {
indices.push(safeLength - 1);
}
return { yMin: yMinCalc, yMax: yMaxCalc, xTickIndices: indices };
}, [chartData, stableYRange]);
// Update stableYRange in useEffect to avoid infinite re-renders
// Use functional update to avoid dependency on stableYRange
useEffect(() => {
if (yMin !== undefined && yMax !== undefined && yMin !== null && yMax !== null && isFinite(yMin) && isFinite(yMax)) {
setStableYRange(prevRange => {
if (!prevRange) {
// Initialize stable range
return { min: yMin, max: yMax };
} else {
// Check if update is needed (5% threshold)
const stableRange = prevRange.max - prevRange.min;
const threshold = stableRange * 0.05;
const needsUpdate =
yMin < (prevRange.min + threshold) ||
yMax > (prevRange.max - threshold);
if (needsUpdate) {
return { min: yMin, max: yMax };
}
// No update needed, return previous range
return prevRange;
}
});
}
}, [yMin, yMax]);
if (!equity || equity.length === 0) {
return (
<div style={{
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: '#cccccc',
fontFamily: '"Courier New", monospace',
fontSize: '12px'
}}>
暂无图表数据
</div>
);
}
const CustomTooltip = ({ active, payload }) => {
if (active && payload && payload.length) {
const isLiveMode = chartTab === 'live';
return (
<div style={{
background: '#000000',
border: '1px solid #333333',
padding: '10px 14px',
fontFamily: '"Courier New", monospace',
fontSize: '10px',
color: '#ffffff'
}}>
<div style={{ fontWeight: 700, marginBottom: '6px', fontSize: '11px' }}>
{payload[0].payload.time}
</div>
{payload.map((entry, index) => (
<div key={index} style={{ color: entry.color, marginTop: '2px' }}>
<span style={{ fontWeight: 700 }}>{entry.name}:</span> {isLiveMode ? `${entry.value.toFixed(2)}%` : `$${formatNumber(entry.value)}`} </div>
))}
</div>
);
}
return null;
};
const CustomDot = ({ dataKey, ...props }) => {
const { cx, cy, payload, index } = props;
const isActive = activePoint === index;
const isLastPoint = index === chartData.length - 1;
// Only show dot for the last point
if (!isLastPoint) {
return null;
}
const colors = {
portfolio: '#00C853',
baseline: '#FF6B00',
baseline_vw: '#9C27B0',
momentum: '#2196F3',
strategy: '#795548'
};
return (
<circle
cx={cx}
cy={cy}
r={isActive ? 6 : 8}
fill={colors[dataKey]}
stroke="#ffffff"
strokeWidth={2}
style={{ cursor: 'pointer' }}
onMouseEnter={() => setActivePoint(index)}
onMouseLeave={() => setActivePoint(null)}
onClick={() => console.log('Clicked point:', { dataKey, ...payload })}
/>
);
};
const CustomXAxisTick = ({ x, y, payload }) => {
const shouldShow = xTickIndices.includes(payload.index);
if (!shouldShow) return null;
return (
<g transform={`translate(${x},${y})`}>
<text
x={0}
y={0}
dy={16}
textAnchor="middle"
fill="#666666"
fontSize="10px"
fontFamily='"Courier New", monospace'
fontWeight="700"
>
{payload.value}
</text>
</g>
);
};
const CustomLegend = ({ payload }) => {
if (!payload || payload.length === 0) return null;
return (
<div style={{
display: 'flex',
flexWrap: 'wrap',
gap: '16px',
padding: '10px 0',
position: 'relative',
fontFamily: '"Courier New", monospace',
fontSize: '11px',
fontWeight: 700,
justifyContent: 'center'
}}>
{payload.map((entry, index) => {
const description = legendDescriptions[entry.value] || '';
const isActive = legendTooltip === entry.value;
return (
<div
key={index}
style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
cursor: 'pointer',
position: 'relative',
padding: '4px 8px',
borderRadius: '4px',
backgroundColor: isActive ? '#f0f0f0' : 'transparent',
transition: 'background-color 0.2s',
userSelect: 'none'
}}
onMouseEnter={() => setLegendTooltip(entry.value)}
onMouseLeave={() => setLegendTooltip(null)}
onClick={(e) => {
e.stopPropagation();
setLegendTooltip(isActive ? null : entry.value);
}}
>
<div
style={{
width: '14px',
height: '3px',
backgroundColor: entry.color,
border: 'none'
}}
/>
<span
style={{
fontFamily: '"Courier New", monospace',
fontSize: '11px',
fontWeight: 700,
color: '#000000'
}}
>
{entry.value}
</span>
{isActive && description && (
<div
style={{
position: 'absolute',
bottom: '100%',
left: 0,
marginBottom: '8px',
padding: '8px 12px',
background: '#000000',
color: '#ffffff',
fontSize: '10px',
fontFamily: '"Courier New", monospace',
whiteSpace: 'normal',
maxWidth: '300px',
zIndex: 1000,
borderRadius: '4px',
boxShadow: '0 2px 8px rgba(0,0,0,0.3)',
pointerEvents: 'none',
lineHeight: 1.4
}}
>
{description}
</div>
)}
</div>
);
})}
</div>
);
};
return (
<ResponsiveContainer width="100%" height="100%">
<LineChart
data={chartData}
margin={{ top: 20, right: 30, bottom: 50, left: 60 }}
>
<XAxis
dataKey="time"
stroke="#666666"
tick={<CustomXAxisTick />}
interval={0}
/>
<YAxis
domain={[yMin, yMax]}
stroke="#000000"
style={{ fontFamily: '"Courier New", monospace', fontSize: '11px', fontWeight: 700 }}
tick={{ fill: '#000000' }}
tickFormatter={(value) => chartTab === 'live' ? `${value.toFixed(2)}%` : formatFullNumber(value)}
width={75}
/>
<Tooltip content={<CustomTooltip />} />
<Legend
content={<CustomLegend />}
/>
{/* Portfolio line */}
<Line
type="linear"
dataKey="portfolio"
name="EvoTraders"
stroke="#00C853"
strokeWidth={2.5}
dot={(props) => <CustomDot {...props} dataKey="portfolio" />}
activeDot={{ r: 6, stroke: '#ffffff', strokeWidth: 2 }}
isAnimationActive={false}
/>
{/* Baseline Equal Weight */}
{baseline && baseline.length > 0 && (
<Line
type="linear"
dataKey="baseline"
name="Buy & Hold (EW)"
stroke="#FF6B00"
strokeWidth={2}
strokeDasharray="5 5"
dot={(props) => <CustomDot {...props} dataKey="baseline" />}
activeDot={{ r: 6, stroke: '#ffffff', strokeWidth: 2 }}
isAnimationActive={false}
/>
)}
{/* Baseline Value Weighted */}
{baseline_vw && baseline_vw.length > 0 && (
<Line
type="linear"
dataKey="baseline_vw"
name="Buy & Hold (VW)"
stroke="#9C27B0"
strokeWidth={2}
strokeDasharray="8 4"
dot={(props) => <CustomDot {...props} dataKey="baseline_vw" />}
activeDot={{ r: 6, stroke: '#ffffff', strokeWidth: 2 }}
isAnimationActive={false}
/>
)}
{/* Momentum Strategy */}
{momentum && momentum.length > 0 && (
<Line
type="linear"
dataKey="momentum"
name="Momentum"
stroke="#2196F3"
strokeWidth={2}
strokeDasharray="3 3"
dot={(props) => <CustomDot {...props} dataKey="momentum" />}
activeDot={{ r: 6, stroke: '#ffffff', strokeWidth: 2 }}
isAnimationActive={false}
/>
)}
{/* Other Strategies */}
{strategies && strategies.length > 0 && (
<Line
type="linear"
dataKey="strategy"
name="Strategy"
stroke="#795548"
strokeWidth={2}
dot={(props) => <CustomDot {...props} dataKey="strategy" />}
activeDot={{ r: 6, stroke: '#ffffff', strokeWidth: 2 }}
isAnimationActive={false}
/>
)}
</LineChart>
</ResponsiveContainer>
);
}