Every trader needs a performance dashboard. Let's build a real-time prop firm performance tracker using React, D3.js, and some clever data visualization techniques.
What We're Building
A dashboard that shows:
- Equity curve with drawdown overlay
- Daily P&L bar chart
- Win rate gauge
- Drawdown proximity meter (how close to the limit)
Project Setup
npx create-react-app trading-dashboard --template typescript
cd trading-dashboard
npm install d3 @types/d3 recharts date-fns
The Data Model
// types.ts
export interface Trade {
id: string;
timestamp: Date;
symbol: string;
side: 'LONG' | 'SHORT';
entryPrice: number;
exitPrice: number;
quantity: number;
pnl: number;
commission: number;
}
export interface AccountState {
balance: number;
peakBalance: number;
drawdownFloor: number;
maxDrawdown: number;
dailyLossLimit: number;
drawdownType: 'eod_trailing' | 'realtime_trailing' | 'static';
}
export interface DailyStats {
date: string;
pnl: number;
trades: number;
winRate: number;
maxDrawdown: number;
}
Equity Curve Component with D3
// EquityCurve.tsx
import React, { useRef, useEffect } from 'react';
import * as d3 from 'd3';
interface Props {
data: { date: Date; balance: number; floor: number }[];
width?: number;
height?: number;
}
const EquityCurve: React.FC<Props> = ({ data, width = 800, height = 400 }) => {
const svgRef = useRef<SVGSVGElement>(null);
useEffect(() => {
if (!svgRef.current || data.length === 0) return;
const svg = d3.select(svgRef.current);
svg.selectAll('*').remove();
const margin = { top: 20, right: 30, bottom: 40, left: 60 };
const w = width - margin.left - margin.right;
const h = height - margin.top - margin.bottom;
const g = svg.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`);
// Scales
const xScale = d3.scaleTime()
.domain(d3.extent(data, d => d.date) as [Date, Date])
.range([0, w]);
const yScale = d3.scaleLinear()
.domain([
d3.min(data, d => Math.min(d.balance, d.floor))! * 0.99,
d3.max(data, d => d.balance)! * 1.01,
])
.range([h, 0]);
// Area between balance and floor (danger zone)
const dangerArea = d3.area<typeof data[0]>()
.x(d => xScale(d.date))
.y0(d => yScale(d.floor))
.y1(d => yScale(Math.min(d.balance, d.floor)));
g.append('path')
.datum(data)
.attr('fill', 'rgba(239, 68, 68, 0.1)')
.attr('d', dangerArea);
// Equity line
const line = d3.line<typeof data[0]>()
.x(d => xScale(d.date))
.y(d => yScale(d.balance))
.curve(d3.curveMonotoneX);
g.append('path')
.datum(data)
.attr('fill', 'none')
.attr('stroke', '#10b981')
.attr('stroke-width', 2)
.attr('d', line);
// Floor line (dashed)
const floorLine = d3.line<typeof data[0]>()
.x(d => xScale(d.date))
.y(d => yScale(d.floor));
g.append('path')
.datum(data)
.attr('fill', 'none')
.attr('stroke', '#ef4444')
.attr('stroke-width', 1.5)
.attr('stroke-dasharray', '5,5')
.attr('d', floorLine);
// Axes
g.append('g')
.attr('transform', `translate(0,${h})`)
.call(d3.axisBottom(xScale).ticks(6));
g.append('g')
.call(d3.axisLeft(yScale).tickFormat(d => `$${d3.format(',.0f')(d as number)}`));
// Legend
const legend = g.append('g').attr('transform', `translate(${w - 150}, 10)`);
legend.append('line').attr('x2', 20).attr('stroke', '#10b981').attr('stroke-width', 2);
legend.append('text').attr('x', 25).attr('y', 4).text('Balance').style('font-size', '12px');
legend.append('line').attr('y1', 20).attr('x2', 20).attr('y2', 20)
.attr('stroke', '#ef4444').attr('stroke-dasharray', '5,5');
legend.append('text').attr('x', 25).attr('y', 24).text('DD Floor').style('font-size', '12px');
}, [data, width, height]);
return <svg ref={svgRef} width={width} height={height} />;
};
Drawdown Proximity Gauge
// DrawdownGauge.tsx
import React from 'react';
interface Props {
currentBalance: number;
drawdownFloor: number;
peakBalance: number;
}
const DrawdownGauge: React.FC<Props> = ({ currentBalance, drawdownFloor, peakBalance }) => {
const totalRange = peakBalance - drawdownFloor;
const currentFromFloor = currentBalance - drawdownFloor;
const proximityPct = Math.max(0, Math.min(100, (currentFromFloor / totalRange) * 100));
const getColor = (pct: number) => {
if (pct > 60) return '#10b981'; // green
if (pct > 30) return '#f59e0b'; // yellow
return '#ef4444'; // red
};
const circumference = 2 * Math.PI * 45;
const dashOffset = circumference * (1 - proximityPct / 100);
return (
<div style={{ textAlign: 'center' }}>
<svg width="120" height="120" viewBox="0 0 120 120">
{/* Background circle */}
<circle cx="60" cy="60" r="45" fill="none" stroke="#e5e7eb" strokeWidth="10" />
{/* Progress */}
<circle
cx="60" cy="60" r="45"
fill="none"
stroke={getColor(proximityPct)}
strokeWidth="10"
strokeLinecap="round"
strokeDasharray={circumference}
strokeDashoffset={dashOffset}
transform="rotate(-90 60 60)"
/>
{/* Text */}
<text x="60" y="55" textAnchor="middle" fontSize="18" fontWeight="bold">
{proximityPct.toFixed(0)}%
</text>
<text x="60" y="72" textAnchor="middle" fontSize="10" fill="#6b7280">
Buffer
</text>
</svg>
<div style={{ fontSize: '12px', color: '#6b7280', marginTop: '4px' }}>
${currentFromFloor.toFixed(0)} above floor
</div>
</div>
);
};
Daily P&L Chart with Recharts
// DailyPnL.tsx
import React from 'react';
import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, Cell } from 'recharts';
interface Props {
data: { date: string; pnl: number }[];
}
const DailyPnL: React.FC<Props> = ({ data }) => (
<ResponsiveContainer width="100%" height={250}>
<BarChart data={data}>
<XAxis dataKey="date" tick={{ fontSize: 11 }} />
<YAxis tickFormatter={(v) => `$${v}`} />
<Tooltip formatter={(value: number) => [`$${value.toFixed(2)}`, 'P&L']} />
<Bar dataKey="pnl" radius={[4, 4, 0, 0]}>
{data.map((entry, idx) => (
<Cell key={idx} fill={entry.pnl >= 0 ? '#10b981' : '#ef4444'} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
);
Stats Summary Component
// StatCard.tsx
interface StatCardProps {
label: string;
value: string;
subtext?: string;
trend?: 'up' | 'down' | 'neutral';
}
const StatCard: React.FC<StatCardProps> = ({ label, value, subtext, trend }) => (
<div style={{
padding: '16px',
borderRadius: '8px',
background: '#f9fafb',
border: '1px solid #e5e7eb',
}}>
<div style={{ fontSize: '12px', color: '#6b7280', marginBottom: '4px' }}>{label}</div>
<div style={{
fontSize: '24px',
fontWeight: 'bold',
color: trend === 'up' ? '#10b981' : trend === 'down' ? '#ef4444' : '#111827',
}}>
{value}
</div>
{subtext && <div style={{ fontSize: '11px', color: '#9ca3af', marginTop: '2px' }}>{subtext}</div>}
</div>
);
// Usage in main dashboard:
// <StatCard label="Total P&L" value="$2,340" trend="up" subtext="+4.68% return" />
// <StatCard label="Win Rate" value="58.3%" trend="neutral" />
// <StatCard label="Max Drawdown" value="$1,200" trend="down" subtext="48% of limit used" />
Key Implementation Notes
- D3 for custom charts — equity curves need precise control over overlays
- Recharts for standard charts — bar charts, line charts are easier with Recharts
- Real-time updates — use WebSocket + React state for live dashboards
- Mobile responsive — CSS Grid with breakpoints for different screen sizes
For traders managing funded accounts, a dashboard like this is essential for staying within your firm's rules. You can find detailed rule breakdowns for different firms at PropFirmKey, including Alpha Futures with their specific drawdown limits and profit targets that you'd configure in your dashboard.
Full source code and live demo link in the comments!
Top comments (0)