diff --git a/docs/indicator-card.md b/docs/indicator-card.md deleted file mode 100644 index 225a20e..0000000 --- a/docs/indicator-card.md +++ /dev/null @@ -1,17 +0,0 @@ -# Indicator Card Sizing - -The indicator cards in the sidebar are laid out using the `.indicator-grid` CSS rule in `frontend/src/styles.css`. Each card is assigned a maximum width via the shared `--sidebar-section-double` variable: - -``` ---sidebar-section-double: calc( - var(--sidebar-section-min-width) * 2 + var(--sidebar-section-gap) -); -``` - -With the current values `--sidebar-section-min-width: 260px` and `--sidebar-section-gap: 8px`, the calculated maximum width for each indicator card is: - -``` -max-width = 260px * 2 + 8px = 528px -``` - -Therefore, `.indicator-card` effectively has a `max-width` of **528px**. diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index f0291ec..bdd7290 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -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; @@ -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[] = []; @@ -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 ( { }, }} > -
-
- - -
- -
-
- - -
- {!response && ( - - SignalSmith Backtester - - Configure parameters on the left and run the backtest to see equity performance and distribution analytics. - - - )} - - {response && ( -
-
- {response.histogram && ( - -
- Return Distribution -
- {(histogramInfoItems.length > 0 || universeFilterTags.length > 0) && ( -
- {[...histogramInfoItems, ...universeFilterTags].map((item) => ( -
- {item.label} - {item.value} -
- ))} -
- )} - -
- )} - - +
+ + +
+ +
+
+ + +
+ {!response && ( + + SignalSmith Backtester + + Configure parameters on the left and run the backtest to see equity performance and distribution analytics. + + + )} + + {response && ( +
+
+ {response.histogram && ( +
- Equity Curve + Return Distribution
- - {highlightMetrics.length > 0 && ( -
- {highlightMetrics.map((metric) => ( -
-

{metric.label}

- {metric.value} + {(histogramInfoItems.length > 0 || universeFilterTags.length > 0) && ( +
+ {[...histogramInfoItems, ...universeFilterTags].map((item) => ( +
+ {item.label} + {item.value}
))}
)} - -
- - {response.indicator_statistics && ( - -
- Indicator Statistics -
- +
)} + + +
+ Equity Curve +
+ +
- )} -
- - -
-
-

By Wendi OUYANG – Chinese University of Hong Kong, Shenzhen

-

Contact: vernonouyang@gmail.com

-
+ + {response.indicator_statistics && ( + +
+ Indicator Statistics +
+ +
+ )} +
+ )} +
+ +
+
+

By Wendi OUYANG – Chinese University of Hong Kong, Shenzhen

+

Contact: vernonouyang@gmail.com

+
); }; diff --git a/frontend/src/components/EquityChart.tsx b/frontend/src/components/EquityChart.tsx index 298b52c..70f3ece 100644 --- a/frontend/src/components/EquityChart.tsx +++ b/frontend/src/components/EquityChart.tsx @@ -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; @@ -22,34 +21,6 @@ const EquityChart = ({ data, loading, onReady }: Props) => { return ; } - 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", @@ -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" } }, }, diff --git a/frontend/src/components/HistogramChart.tsx b/frontend/src/components/HistogramChart.tsx index b48a8b9..397d16a 100644 --- a/frontend/src/components/HistogramChart.tsx +++ b/frontend/src/components/HistogramChart.tsx @@ -91,6 +91,7 @@ const HistogramChart = ({ data, loading, onReady, height = 400 }: Props) => { ); })} +
)}
diff --git a/frontend/src/components/SidebarForm.tsx b/frontend/src/components/SidebarForm.tsx index ea4d9ad..87c5aaf 100644 --- a/frontend/src/components/SidebarForm.tsx +++ b/frontend/src/components/SidebarForm.tsx @@ -24,11 +24,9 @@ const DEFAULT_PRESET_KEY = "backtest-sidebar-preset"; const MIN_DATE = dayjs("2020-01-01"); const LOOKBACK_YEARS = 5; -type IndicatorKey = "rsi" | "macd" | "obv" | "ema" | "adx" | "aroon" | "stoch"; +type IndicatorKey = "rsi" | "macd" | "obv" | "ema" | "adx" | "aroon" | "stoch" | "signals"; -type InfoModalKey = IndicatorKey | "signals" | "execution" | "universe"; - -const INDICATOR_KEYS: IndicatorKey[] = ["rsi", "macd", "obv", "aroon", "ema", "adx", "stoch"]; +type InfoModalKey = IndicatorKey | "execution" | "universe"; interface SidebarFormProps { loading: boolean; @@ -57,7 +55,6 @@ const SidebarForm = ({ loading, onSubmit }: SidebarFormProps) => { const [meta, setMeta] = useState({ sectors: [], mcap_buckets: [] }); const [activeInfo, setActiveInfo] = useState(null); const [showUniverseFilters, setShowUniverseFilters] = useState(false); - const [showIndicatorDetails, setShowIndicatorDetails] = useState(false); const openInfo = (key: InfoModalKey) => { setActiveInfo(key); @@ -113,7 +110,6 @@ const SidebarForm = ({ loading, onSubmit }: SidebarFormProps) => { const handleReset = () => { form.resetFields(); setShowUniverseFilters(false); - setShowIndicatorDetails(false); }; const handleSavePreset = () => { @@ -612,29 +608,7 @@ const SidebarForm = ({ loading, onSubmit }: SidebarFormProps) => {
- setShowIndicatorDetails((prev) => !prev)}> - {showIndicatorDetails ? "Hide" : "Describe"} - - } - > - {showIndicatorDetails && ( -
- {INDICATOR_KEYS.map((key) => ( -
- - {infoTitles[key]} - - {renderInfoContent(key)} -
- ))} -
- )} +
@@ -643,6 +617,9 @@ const SidebarForm = ({ loading, onSubmit }: SidebarFormProps) => { +
@@ -675,6 +652,9 @@ const SidebarForm = ({ loading, onSubmit }: SidebarFormProps) => { +
@@ -706,6 +686,9 @@ const SidebarForm = ({ loading, onSubmit }: SidebarFormProps) => { +
@@ -742,6 +725,9 @@ const SidebarForm = ({ loading, onSubmit }: SidebarFormProps) => { +
@@ -757,45 +743,47 @@ const SidebarForm = ({ loading, onSubmit }: SidebarFormProps) => {
-
-
-
-
- EMA - - - - - -
-
- - - - - - -
-
-
-
-
- ADX - - - - - -
-
- - - - - - -
-
+
+
+ ADX + + + + + + +
+
+ + + + + + +
+
+ +
+
+ EMA + + + + + + +
+
+ + + + + +
@@ -806,6 +794,9 @@ const SidebarForm = ({ loading, onSubmit }: SidebarFormProps) => { +
diff --git a/frontend/src/styles.css b/frontend/src/styles.css index a920502..07dfe18 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -2,35 +2,21 @@ font-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; color: #0f172a; background-color: #f3f5ff; - --app-scale: 0.7; - font-size: clamp(12px, 0.9vw + 8px, 15px); } body { margin: 0; min-height: 100vh; - font-size: clamp(0.9rem, 0.85rem + 0.15vw, 0.98rem); - overflow-x: hidden; } #root { min-height: 100vh; } -.app-scale-wrapper { - transform: scale(var(--app-scale)); - transform-origin: top left; - width: calc(100% / var(--app-scale)); - min-height: calc(100vh / var(--app-scale)); - display: flex; - flex-direction: column; -} - .app-shell { - height: calc(100vh / var(--app-scale)); + height: 100vh; width: 100%; background: #f5f7ff; - font-size: clamp(0.92rem, 0.88rem + 0.15vw, 1rem); } .panel { @@ -40,31 +26,24 @@ body { .panel--sidebar { background: #f8f9ff; + max-width: 50vw; } .sidebar-panel { width: 100%; height: 100%; overflow-y: auto; - padding: 2px 4px 10px; + padding: 6px; box-sizing: border-box; } .sidebar-form { - --sidebar-section-min-width: 240px; - --sidebar-section-gap: 8px; - --sidebar-section-double: calc( - var(--sidebar-section-min-width) * 2 + var(--sidebar-section-gap) - ); - --indicator-card-min: 220px; - --indicator-card-max: 280px; height: 100%; overflow-y: auto; - padding: 0 4px 8px; + padding: 0 6px 10px; display: flex; flex-direction: column; - gap: var(--sidebar-section-gap); - align-items: stretch; + gap: 8px; } .sidebar-form .form-row { @@ -91,9 +70,8 @@ body { .sidebar-form .form-grid { display: grid; - gap: var(--sidebar-section-gap); - margin-bottom: var(--sidebar-section-gap); - align-items: end; + gap: 8px; + margin-bottom: 8px; } .sidebar-form .form-grid:last-child { @@ -109,10 +87,9 @@ body { } .sidebar-form .form-grid--capital-range { - grid-template-columns: repeat( - auto-fit, - minmax(clamp(200px, 40vw, 320px), 1fr) - ); + grid-template-columns: minmax(110px, 0.45fr) minmax(200px, 0.55fr); + align-items: end; + column-gap: 10px; } .sidebar-form .form-grid--capital-range .form-grid__item--range .ant-picker, @@ -121,10 +98,7 @@ body { } .sidebar-form .form-grid--four { - grid-template-columns: repeat( - auto-fit, - minmax(clamp(150px, 24vw, 220px), 1fr) - ); + grid-template-columns: repeat(4, minmax(0, 1fr)); } @media (max-width: 1200px) { @@ -145,10 +119,6 @@ body { box-shadow: 0 6px 16px rgba(46, 92, 255, 0.08); } -.sidebar-card { - width: 100%; -} - .sidebar-form .ant-card-head { min-height: 30px; padding: 0 8px; @@ -163,16 +133,15 @@ body { padding: 8px; } - .sidebar-card-row { - display: grid; - gap: var(--sidebar-section-gap); - width: 100%; - margin: 0; - grid-template-columns: repeat( - auto-fit, - minmax(clamp(260px, 48%, 420px), 1fr) - ); + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.sidebar-card--half { + flex: 1 1 280px; + max-width: 320px; } .sidebar-form .ant-form-item { @@ -185,12 +154,12 @@ body { } .form-grid--universe { - grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 6px; } .form-grid--signals { - grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); + grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 6px; } @@ -206,7 +175,7 @@ body { @media (max-width: 640px) { .form-grid--universe { - grid-template-columns: 1fr; + grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); } } @@ -466,54 +435,26 @@ body { margin-bottom: 12px; } - .indicator-grid { - --indicator-card-gap: var(--sidebar-section-gap); - --indicator-field-width: 124px; - display: grid; - gap: var(--indicator-card-gap); - width: 100%; - grid-auto-flow: row dense; - grid-template-columns: repeat( - auto-fit, - minmax(var(--indicator-card-min), max-content) - ); - justify-content: flex-start; - justify-items: start; -} - -.indicator-describe { - background: #eef2ff; - border: 1px solid #dbe3ff; - border-radius: 10px; - padding: 10px 12px; display: flex; - flex-direction: column; - gap: 12px; - margin-bottom: 12px; -} - -.indicator-describe__section + .indicator-describe__section { - border-top: 1px solid #d1dcff; - padding-top: 12px; -} - -.indicator-describe__heading { - margin: 0 0 4px 0; - font-size: 12px; - color: #1e3a8a; + flex-wrap: wrap; + gap: 8px; } .indicator-grid__item { background: #f9faff; border: 1px solid #e2e7ff; border-radius: 10px; - padding: 6px 8px; + padding: 6px; display: flex; flex-direction: column; - min-width: 0; - width: clamp(var(--indicator-card-min), 24vw, var(--indicator-card-max)); - max-width: var(--indicator-card-max); + flex: 0 0 152px; + width: 152px; + max-width: 152px; +} + +.indicator-grid__item--compact { + padding: 6px; } .indicator-grid__item .ant-form-item { @@ -542,74 +483,26 @@ body { .indicator-fields { display: grid; - grid-auto-flow: row dense; - grid-template-columns: repeat( - auto-fit, - minmax(var(--indicator-field-width), max-content) - ); - grid-auto-columns: minmax(var(--indicator-field-width), max-content); - justify-content: flex-start; + grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 6px; - align-items: start; -} - -.indicator-fields--compact { - gap: 4px; } .indicator-fields--single { - grid-template-columns: minmax(var(--indicator-field-width), max-content); + grid-template-columns: 1fr; } .indicator-fields--triple { - grid-template-columns: repeat( - auto-fit, - minmax(var(--indicator-field-width), max-content) - ); + grid-template-columns: repeat(2, minmax(0, 1fr)); } .indicator-field { margin-bottom: 0 !important; - width: var(--indicator-field-width); - min-width: var(--indicator-field-width); - max-width: 100%; } .indicator-field--full { grid-column: 1 / -1; - width: 100%; - max-width: none; - min-width: 0; -} - -.indicator-field .ant-input-number, -.indicator-field .ant-select, -.indicator-field .ant-picker, -.indicator-field .ant-input { - width: 100%; -} - - -.indicator-stack { - display: flex; - flex-direction: column; - gap: 10px; -} - -.indicator-stack__section { - display: flex; - flex-direction: column; - gap: 6px; } -.indicator-stack__divider { - height: 1px; - background: linear-gradient(90deg, rgba(29, 63, 174, 0.2), rgba(29, 63, 174, 0)); -} - -.indicator-header--stacked { - margin-bottom: 0; -} .indicator-hint { font-size: 11px; @@ -621,28 +514,20 @@ body { color: #1d3fae; } -@media (max-width: 900px) { - .indicator-grid { - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); - } - - .indicator-grid__item { - width: min(100%, var(--indicator-card-max)); - max-width: var(--indicator-card-max); - } -} - @media (max-width: 640px) { - .sidebar-card-row { - grid-template-columns: 1fr; + .indicator-grid__item { + flex-basis: 100%; + max-width: 100%; + min-width: 0; + width: 100%; } .indicator-fields { - grid-template-columns: minmax(var(--indicator-field-width), max-content); + grid-template-columns: 1fr; } .indicator-fields--triple { - grid-template-columns: minmax(var(--indicator-field-width), max-content); + grid-template-columns: 1fr; } } @@ -682,10 +567,6 @@ body { gap: 10px; } -.metrics-grid--compact { - gap: 8px; -} - .metrics-item { padding: 10px 12px; border-radius: 10px; @@ -693,10 +574,6 @@ body { border: 1px solid #dbe1ff; } -.metrics-item--compact { - padding: 8px 10px; -} - .metrics-item h4 { margin: 0 0 4px 0; font-weight: 600; @@ -710,10 +587,6 @@ body { color: #111827; } -.equity-metrics { - margin-top: 4px; -} - .resize-handle { width: 4px; background: linear-gradient(to bottom, #dbe1ff, #a0b1ff);