Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 0 additions & 17 deletions docs/indicator-card.md

This file was deleted.

158 changes: 58 additions & 100 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { api } from "./api/client";
import HistogramChart from "./components/HistogramChart";
import IndicatorStatsTable from "./components/IndicatorStatsTable";
import EquityChart from "./components/EquityChart";
import { formatCurrency, formatNumber, formatPercent } from "./utils/format";
import { formatCurrency, formatNumber } from "./utils/format";

const { Title, Text } = Typography;

Expand All @@ -16,22 +16,6 @@ interface InfoTag {
value: string;
}

interface MetricConfig {
key: string;
label: string;
format: (value: number) => string;
}

const EQUITY_METRICS: MetricConfig[] = [
{ key: "sharpe", label: "Sharpe Ratio", format: (value) => value.toFixed(2) },
{ key: "sortino", label: "Sortino Ratio", format: (value) => value.toFixed(2) },
{ key: "annualized_return", label: "Annualized Return", format: (value) => formatPercent(value, 2) },
{ key: "annualized_vol", label: "Annualized Volatility", format: (value) => formatPercent(value, 2) },
{ key: "max_drawdown", label: "Max Drawdown", format: (value) => formatPercent(value, 2) },
{ key: "win_rate", label: "Win Rate", format: (value) => formatPercent(value, 2) },
{ key: "avg_trade_return", label: "Avg Trade Return", format: (value) => formatPercent(value, 2) },
];

const formatFilters = (filters?: Filters | null): InfoTag[] => {
if (!filters) return [];
const tags: InfoTag[] = [];
Expand Down Expand Up @@ -100,20 +84,6 @@ const App = () => {

const universeFilterTags = formatFilters(lastRunConfig?.filters);

const highlightMetrics: { key: string; label: string; value: string }[] = response?.metrics
? (EQUITY_METRICS.map((config) => {
const rawValue = response.metrics[config.key];
if (typeof rawValue !== "number" || Number.isNaN(rawValue)) {
return null;
}
return {
key: config.key,
label: config.label,
value: config.format(rawValue),
};
}).filter(Boolean) as { key: string; label: string; value: string }[])
: [];

return (
<ConfigProvider
theme={{
Expand All @@ -123,85 +93,73 @@ const App = () => {
},
}}
>
<div className="app-scale-wrapper">
<div className="app-shell">
<PanelGroup direction="horizontal">
<Panel defaultSize={35} minSize={30} maxSize={65} className="panel panel--sidebar">
<div className="sidebar-panel">
<SidebarForm loading={loading} onSubmit={handleSubmit} />
</div>
</Panel>
<PanelResizeHandle className="resize-handle" />
<Panel defaultSize={65} minSize={35} className="panel panel--content">
<div className="results-panel">
{!response && (
<Card className="result-card intro-card">
<Title level={3}>SignalSmith Backtester</Title>
<Text type="secondary">
Configure parameters on the left and run the backtest to see equity performance and distribution analytics.
</Text>
</Card>
)}

{response && (
<div className="results-container">
<div className="results-grid results-grid--charts">
{response.histogram && (
<Card className="result-card result-card--chart histogram-card">
<div className="card-header">
<Title level={4}>Return Distribution</Title>
</div>
{(histogramInfoItems.length > 0 || universeFilterTags.length > 0) && (
<div className="histogram-info">
{[...histogramInfoItems, ...universeFilterTags].map((item) => (
<div key={`${item.label}-${item.value}`} className="info-pill">
<span className="info-pill__label">{item.label}</span>
<span className="info-pill__value">{item.value}</span>
</div>
))}
</div>
)}
<HistogramChart data={response.histogram} loading={loading} />
</Card>
)}

<Card className="result-card result-card--chart">
<div className="app-shell">
<PanelGroup direction="horizontal">
<Panel defaultSize={28} minSize={18} maxSize={50} className="panel panel--sidebar">
<div className="sidebar-panel">
<SidebarForm loading={loading} onSubmit={handleSubmit} />
</div>
</Panel>
<PanelResizeHandle className="resize-handle" />
<Panel defaultSize={72} minSize={40} className="panel panel--content">
<div className="results-panel">
{!response && (
<Card className="result-card intro-card">
<Title level={3}>SignalSmith Backtester</Title>
<Text type="secondary">
Configure parameters on the left and run the backtest to see equity performance and distribution analytics.
</Text>
</Card>
)}

{response && (
<div className="results-container">
<div className="results-grid results-grid--charts">
{response.histogram && (
<Card className="result-card result-card--chart histogram-card">
<div className="card-header">
<Title level={4}>Equity Curve</Title>
<Title level={4}>Return Distribution</Title>
</div>
<EquityChart data={response.equity_curve} loading={loading} />
{highlightMetrics.length > 0 && (
<div className="metrics-grid metrics-grid--compact equity-metrics">
{highlightMetrics.map((metric) => (
<div key={metric.key} className="metrics-item metrics-item--compact">
<h4>{metric.label}</h4>
<span>{metric.value}</span>
{(histogramInfoItems.length > 0 || universeFilterTags.length > 0) && (
<div className="histogram-info">
{[...histogramInfoItems, ...universeFilterTags].map((item) => (
<div key={`${item.label}-${item.value}`} className="info-pill">
<span className="info-pill__label">{item.label}</span>
<span className="info-pill__value">{item.value}</span>
</div>
))}
</div>
)}
</Card>
</div>

{response.indicator_statistics && (
<Card className="result-card result-card--wide">
<div className="card-header">
<Title level={4}>Indicator Statistics</Title>
</div>
<IndicatorStatsTable stats={response.indicator_statistics} />
<HistogramChart data={response.histogram} loading={loading} />
</Card>
)}

<Card className="result-card result-card--chart">
<div className="card-header">
<Title level={4}>Equity Curve</Title>
</div>
<EquityChart data={response.equity_curve} loading={loading} />
</Card>
</div>
)}
</div>
</Panel>
</PanelGroup>
</div>
<footer className="app-footer">
<p>By Wendi OUYANG – Chinese University of Hong Kong, Shenzhen</p>
<p>Contact: vernonouyang@gmail.com</p>
</footer>

{response.indicator_statistics && (
<Card className="result-card result-card--wide">
<div className="card-header">
<Title level={4}>Indicator Statistics</Title>
</div>
<IndicatorStatsTable stats={response.indicator_statistics} />
</Card>
)}
</div>
)}
</div>
</Panel>
</PanelGroup>
</div>
<footer className="app-footer">
<p>By Wendi OUYANG – Chinese University of Hong Kong, Shenzhen</p>
<p>Contact: vernonouyang@gmail.com</p>
</footer>
</ConfigProvider>
);
};
Expand Down
38 changes: 2 additions & 36 deletions frontend/src/components/EquityChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ import ReactECharts from "echarts-for-react";
import type { ECharts } from "echarts";
import { Spin, Empty } from "antd";
import { TimeSeries } from "../types";
import { formatCurrency } from "../utils/format";
import dayjs from "dayjs";
import { formatCurrency, formatDateYYMMDD } from "../utils/format";

interface Props {
data?: TimeSeries | null;
Expand All @@ -22,34 +21,6 @@ const EquityChart = ({ data, loading, onReady }: Props) => {
return <Empty description="No equity data" />;
}

const firstDate = dayjs(data.dates[0]);
const lastDate = dayjs(data.dates[data.dates.length - 1]);
const totalMonths = lastDate.diff(firstDate, "month", true);
const useQuarterTicks = totalMonths > 18;

const shouldShowTick = (value: string) => {
const parsed = dayjs(value);
if (!parsed.isValid()) return false;
const monthEnd = parsed.endOf("month");
if (parsed.date() !== monthEnd.date()) {
return false;
}
if (!useQuarterTicks) {
return true;
}
return parsed.month() % 3 === 2;
};

const formatTickLabel = (value: string) => {
const parsed = dayjs(value);
if (!parsed.isValid()) return value;
if (useQuarterTicks) {
const quarter = Math.floor(parsed.month() / 3) + 1;
return `${parsed.format("YYYY")} Q${quarter}`;
}
return parsed.format("MMM YY");
};

const option = {
tooltip: {
trigger: "axis",
Expand Down Expand Up @@ -79,13 +50,8 @@ const EquityChart = ({ data, loading, onReady }: Props) => {
type: "category",
data: data.dates,
axisLabel: {
formatter: (value: string) => (shouldShowTick(value) ? formatTickLabel(value) : ""),
formatter: (value: string) => formatDateYYMMDD(value),
hideOverlap: true,
margin: 16,
},
axisTick: {
alignWithLabel: true,
interval: (_index: number, value: string) => shouldShowTick(value),
},
splitLine: { show: true, lineStyle: { color: "#e2e8f0" } },
},
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/HistogramChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ const HistogramChart = ({ data, loading, onReady, height = 400 }: Props) => {
);
})}
<Statistic title="Samples" value={data.sample_size} />
<Statistic title="Bins" value={data.bin_count} />
</div>
)}
</div>
Expand Down
Loading