Skip to content
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,11 @@ python -m quant_research_starter.cli compute-factors -d data_sample/sample_price
# run a backtest
python -m quant_research_starter.cli backtest -d data_sample/sample_prices.csv -s output/factors.csv -o output/backtest_results.json

# optional: start the Streamlit dashboard
# DISCLAIMER: OLD VERSION
# optional: start the Streamlit dashboard, if on main stream
streamlit run src/quant_research_starter/dashboard/streamlit_app.py
# NEW VERSION: if streamlit is in legacy folder
streamlit run legacy/streamlit/streamlit_app.py
```

---
Expand Down
32 changes: 32 additions & 0 deletions src/quant_research_starter/frontend/cauweb/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"name": "@qrs/cauweb",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"lint": "eslint src --ext .ts,.tsx"
},
"dependencies": {
"@tailwindcss/vite": "^4.1.17",
"chart.js": "^4.5.1",
"lucide-react": "^0.263.1",
"react": "^18.2.0",
"react-chartjs-2": "^5.3.1",
"react-dom": "^18.2.0",
"react-router-dom": "^6.8.0"
},
"devDependencies": {
"@tailwindcss/cli": "^4.1.17",
"@tailwindcss/postcss": "^4.1.17",
"@types/react": "^18.3.26",
"@types/react-dom": "^18.3.7",
"@vitejs/plugin-react": "^4.1.0",
"autoprefixer": "^10.4.21",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.17",
"typescript": "^5.9.3",
"vite": "^5.0.0"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
import React from 'react';

interface ChartData {
date: string;
value: number;
}

interface PerformanceChartProps {
portfolioData: ChartData[];
benchmarkData: ChartData[];
height?: number;
}

export const PerformanceChart: React.FC<PerformanceChartProps> = ({
portfolioData,
benchmarkData,
height = 300
}) => {
if (!portfolioData.length || !benchmarkData.length) {
return (
<div className="flex items-center justify-center h-64 bg-dark-50/50 border border-dark-300 rounded-xl">
<div className="text-center text-dark-500">
<div className="text-4xl mb-2">📊</div>
<p>No chart data available</p>
</div>
</div>
);
}

// Calculate chart dimensions and scales
const chartWidth = 800;
const chartHeight = height;
const padding = { top: 40, right: 40, bottom: 40, left: 60 };

// Find min and max values for scaling
const allValues = [...portfolioData.map(d => d.value), ...benchmarkData.map(d => d.value)];
const maxValue = Math.max(...allValues);
const minValue = Math.min(...allValues);
const valueRange = maxValue - minValue;

// Scale functions
const scaleX = (index: number) =>
padding.left + (index / (portfolioData.length - 1)) * (chartWidth - padding.left - padding.right);

const scaleY = (value: number) =>
chartHeight - padding.bottom - ((value - minValue) / valueRange) * (chartHeight - padding.top - padding.bottom);

// Generate SVG path data
const generatePath = (data: ChartData[]) => {
if (data.length === 0) return '';

let path = `M ${scaleX(0)} ${scaleY(data[0].value)}`;

for (let i = 1; i < data.length; i++) {
path += ` L ${scaleX(i)} ${scaleY(data[i].value)}`;
}

return path;
};

const portfolioPath = generatePath(portfolioData);
const benchmarkPath = generatePath(benchmarkData);

// Generate grid lines and labels
const gridLines = [];
const yLabels = [];
const numGridLines = 5;

for (let i = 0; i <= numGridLines; i++) {
const value = minValue + (i / numGridLines) * valueRange;
const y = scaleY(value);

gridLines.push(
<line
key={`grid-${i}`}
x1={padding.left}
y1={y}
x2={chartWidth - padding.right}
y2={y}
className="stroke-dark-400/30 stroke-1"
/>
);

yLabels.push(
<text
key={`label-${i}`}
x={padding.left - 10}
y={y}
className="fill-dark-500 text-xs font-inter"
textAnchor="end"
dominantBaseline="middle"
>
${Math.round(value).toLocaleString()}
</text>
);
}

// Generate x-axis labels (dates)
const xLabels = [];
const labelInterval = Math.max(1, Math.floor(portfolioData.length / 6));

for (let i = 0; i < portfolioData.length; i += labelInterval) {
const date = new Date(portfolioData[i].date);
const label = date.toLocaleDateString('en-US', {
year: '2-digit',
month: 'short'
});

xLabels.push(
<text
key={`xlabel-${i}`}
x={scaleX(i)}
y={chartHeight - padding.bottom + 20}
className="fill-dark-500 text-[10px] font-inter"
textAnchor="middle"
>
{label}
</text>
);
}

// Calculate performance metrics
const portfolioStart = portfolioData[0].value;
const portfolioEnd = portfolioData[portfolioData.length - 1].value;
const portfolioReturn = ((portfolioEnd - portfolioStart) / portfolioStart) * 100;

const benchmarkStart = benchmarkData[0].value;
const benchmarkEnd = benchmarkData[benchmarkData.length - 1].value;
const benchmarkReturn = ((benchmarkEnd - benchmarkStart) / benchmarkStart) * 100;

return (
<div className="bg-gradient-to-br from-dark-50/95 to-dark-100/98 border border-dark-400/20 rounded-2xl p-6 backdrop-blur-xl transition-all duration-300 hover:border-blue-500/30 hover:shadow-2xl">
{/* Chart Header */}
<div className="flex flex-col lg:flex-row lg:justify-between lg:items-start gap-4 mb-6">
<div>
<h3 className="text-white text-xl font-semibold mb-1">Portfolio Performance vs Benchmark</h3>
<div className="text-dark-500 text-sm">
{portfolioData[0]?.date} to {portfolioData[portfolioData.length - 1]?.date}
</div>
</div>

<div className="flex flex-col sm:flex-row gap-4 sm:gap-6">
<div className="flex flex-col gap-1">
<span className="text-dark-500 text-xs uppercase tracking-wide">Portfolio Return:</span>
<span className={`text-lg font-semibold ${portfolioReturn >= 0 ? 'text-green-400' : 'text-red-400'}`}>
{portfolioReturn >= 0 ? '+' : ''}{portfolioReturn.toFixed(2)}%
</span>
</div>

<div className="flex flex-col gap-1">
<span className="text-dark-500 text-xs uppercase tracking-wide">Benchmark Return:</span>
<span className={`text-lg font-semibold ${benchmarkReturn >= 0 ? 'text-green-400' : 'text-red-400'}`}>
{benchmarkReturn >= 0 ? '+' : ''}{benchmarkReturn.toFixed(2)}%
</span>
</div>

<div className="flex flex-col gap-1">
<span className="text-dark-500 text-xs uppercase tracking-wide">Outperformance:</span>
<span className={`text-lg font-semibold ${portfolioReturn >= benchmarkReturn ? 'text-green-400' : 'text-red-400'}`}>
{(portfolioReturn - benchmarkReturn).toFixed(2)}%
</span>
</div>
</div>
</div>

{/* Chart Container */}
<div className="bg-white/5 border border-dark-400/10 rounded-xl p-4 my-4 transition-colors duration-300 hover:border-dark-400/20">
<svg
width="100%"
height={chartHeight}
viewBox={`0 0 ${chartWidth} ${chartHeight}`}
className="block max-w-full h-auto"
>
{/* Background */}
<rect width="100%" height="100%" fill="transparent" />

{/* Grid lines */}
{gridLines}

{/* Y-axis labels */}
{yLabels}

{/* X-axis labels */}
{xLabels}

{/* Axis lines */}
<line
x1={padding.left}
y1={padding.top}
x2={padding.left}
y2={chartHeight - padding.bottom}
className="stroke-dark-400/50 stroke-2"
/>
<line
x1={padding.left}
y1={chartHeight - padding.bottom}
x2={chartWidth - padding.right}
y2={chartHeight - padding.bottom}
className="stroke-dark-400/50 stroke-2"
/>

{/* Portfolio line with glow effect */}
<defs>
<filter id="portfolioGlow" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur stdDeviation="4" result="coloredBlur"/>
<feMerge>
<feMergeNode in="coloredBlur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>

<path
d={portfolioPath}
className="stroke-blue-500 fill-none stroke-[3]"
strokeLinecap="round"
strokeLinejoin="round"
filter="url(#portfolioGlow)"
/>

{/* Benchmark line */}
<path
d={benchmarkPath}
className="stroke-green-500 fill-none stroke-2"
strokeLinecap="round"
strokeLinejoin="round"
strokeDasharray="5,5"
/>

{/* Portfolio points */}
{portfolioData.map((point, index) => (
<circle
key={`portfolio-${index}`}
cx={scaleX(index)}
cy={scaleY(point.value)}
r="4"
className="fill-blue-500 stroke-white stroke-2 transition-all duration-200 hover:r-6 hover:fill-white hover:stroke-blue-500"
/>
))}

{/* Benchmark points */}
{benchmarkData.map((point, index) => (
<circle
key={`benchmark-${index}`}
cx={scaleX(index)}
cy={scaleY(point.value)}
r="3"
className="fill-green-500 stroke-white stroke-[1.5] transition-all duration-200 hover:r-5 hover:fill-white hover:stroke-green-500"
/>
))}
</svg>
</div>

{/* Chart Legend */}
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center mt-4 pt-4 border-t border-dark-400/20">
<div className="flex items-center gap-3 bg-white/5 px-4 py-2 rounded-lg border border-dark-400/10">
<div className="w-4 h-1 bg-gradient-to-r from-blue-500 to-purple-600 rounded-full"></div>
<span className="text-dark-500 text-sm">
Strategy Portfolio (${portfolioEnd.toLocaleString()})
</span>
</div>

<div className="flex items-center gap-3 bg-white/5 px-4 py-2 rounded-lg border border-dark-400/10">
<div className="w-4 h-1 bg-gradient-to-r from-green-500 to-green-600 rounded-full"></div>
<span className="text-dark-500 text-sm">
Benchmark - SPY (${benchmarkEnd.toLocaleString()})
</span>
</div>
</div>
</div>
);
};

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading