DEV Community

Propfirmkey
Propfirmkey

Posted on

Building a Prop Firm Performance Dashboard with React & D3

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
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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} />;
};
Enter fullscreen mode Exit fullscreen mode

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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>
);
Enter fullscreen mode Exit fullscreen mode

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" />
Enter fullscreen mode Exit fullscreen mode

Key Implementation Notes

  1. D3 for custom charts — equity curves need precise control over overlays
  2. Recharts for standard charts — bar charts, line charts are easier with Recharts
  3. Real-time updates — use WebSocket + React state for live dashboards
  4. 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)