From b315e531df78733820969e04a0d5d9b6afee572a Mon Sep 17 00:00:00 2001 From: shy3130 Date: Mon, 29 Jun 2026 22:45:16 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=9B=9E=E6=B5=8B=E9=A1=B5=E5=8A=A0?= =?UTF-8?q?=E9=A2=84=E7=83=AD=E6=9C=9F=E8=AF=B4=E6=98=8E=E5=BE=BD=E6=A0=87?= =?UTF-8?q?=E4=B8=8E=E6=BB=91=E7=82=B9=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 回测区间旁加「预热≥120天」徽标,点击弹泡说明开头无交易原因 - 佣金旁新增滑点(万分之)输入,持久化并接入回测请求 - Auth 设密码页加访问密码部署文档链接 --- frontend/src/components/WarmupBadge.tsx | 106 ++++++++++++++++++ frontend/src/lib/storage.ts | 1 + frontend/src/pages/Auth.tsx | 19 +++- .../src/pages/backtest/StrategyBacktest.tsx | 13 ++- 4 files changed, 135 insertions(+), 4 deletions(-) create mode 100644 frontend/src/components/WarmupBadge.tsx diff --git a/frontend/src/components/WarmupBadge.tsx b/frontend/src/components/WarmupBadge.tsx new file mode 100644 index 0000000..2f36d84 --- /dev/null +++ b/frontend/src/components/WarmupBadge.tsx @@ -0,0 +1,106 @@ +/** + * 回测预热期徽标 — 点击弹出说明气泡。 + * + * 解释「回测开头几个月没有交易」这一高频疑问: 技术指标需要历史数据预热, + * 系统会自动在回测起点之前多取约 120 天 (≈4 个月) 数据; 若本地数据恰好从 + * 起点才开始, 开头几个月指标算不出、信号不触发, 属正常现象。 + * + * 实现要点: + * - 点击触发 (非 hover), 移动端友好 + * - 用 createPortal 渲染到 body, 绕开父容器 overflow 裁剪 (回测配置面板有 overflow-y-auto) + * - 全屏透明遮罩点击关闭 + ESC 关闭 + * - 气泡位置 = 锚点 rect 实时计算, 自动判断向左/向右展开避免溢出屏幕 + */ +import { useEffect, useLayoutEffect, useRef, useState } from 'react' +import { createPortal } from 'react-dom' +import { AnimatePresence, motion } from 'framer-motion' +import { Info } from 'lucide-react' + +interface Pos { top: number; left: number } + +export function WarmupBadge() { + const [open, setOpen] = useState(false) + const anchorRef = useRef(null) + const [pos, setPos] = useState({ top: 0, left: 0 }) + + // 打开时根据锚点 rect 计算气泡位置 (向下方弹出) + useLayoutEffect(() => { + if (!open || !anchorRef.current) return + const rect = anchorRef.current.getBoundingClientRect() + const POPUP_W = 272 + const GAP = 8 + // 优先左对齐锚点; 右侧不够则右对齐; 兜底贴左边 + let left = rect.left + if (left + POPUP_W > window.innerWidth - 8) { + left = rect.right - POPUP_W + } + left = Math.max(8, left) + setPos({ top: rect.bottom + GAP, left }) + }, [open]) + + // ESC 关闭 + useEffect(() => { + if (!open) return + const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') setOpen(false) } + document.addEventListener('keydown', onKey) + return () => document.removeEventListener('keydown', onKey) + }, [open]) + + return ( + <> + + + {createPortal( + + {open && ( + <> + {/* 全屏透明遮罩: 点击关闭 */} +
setOpen(false)} + /> + {/* 气泡: 绝对定位到 body, 绕开 overflow 裁剪 */} + e.stopPropagation()} + > +
为什么开头几个月可能没有交易?
+

+ 技术指标 (MA / MACD / RSI 等) 需要历史数据才能算出。系统会自动在回测起点之前多取约 + 120 天 (≈4 个月) 数据做预热。 +

+

+ 若本地数据恰好从回测起点才开始, 开头几个月指标算不出、信号不触发, + 属正常现象, 不是 bug。等数据攒够后自然开始产生交易。 +

+
+ 解决: 把历史数据补到回测起点之前至少半年, 或把起点往后挪。 +
+ {/* 小箭头指向锚点 */} +
+ + + )} + , + document.body, + )} + + ) +} diff --git a/frontend/src/lib/storage.ts b/frontend/src/lib/storage.ts index 8f368ef..1b3db9b 100644 --- a/frontend/src/lib/storage.ts +++ b/frontend/src/lib/storage.ts @@ -91,6 +91,7 @@ export const storage = { entryFill: 'close_t' | 'open_t+1' exitFill: 'close_t' | 'open_t+1' fees: string + slippage: string maxPositions: string maxExposure: string initialCapital: string diff --git a/frontend/src/pages/Auth.tsx b/frontend/src/pages/Auth.tsx index 87a360e..cdc69ba 100644 --- a/frontend/src/pages/Auth.tsx +++ b/frontend/src/pages/Auth.tsx @@ -166,9 +166,22 @@ export function Auth() { {/* 提示: 设密码模式告知本机限制 */} {isSetup && ( -

- 出于安全考虑, 首次设置密码需在服务器本机或内网访问时操作。公网环境下仅可登录。 -

+
+

+ 出于安全考虑, 首次设置密码需在服务器本机或内网访问时操作。公网环境下仅可登录。 +

+

+ 详细配置说明见{' '} + + 访问密码部署文档 + +

+
)}
diff --git a/frontend/src/pages/backtest/StrategyBacktest.tsx b/frontend/src/pages/backtest/StrategyBacktest.tsx index 7eca937..bc951c5 100644 --- a/frontend/src/pages/backtest/StrategyBacktest.tsx +++ b/frontend/src/pages/backtest/StrategyBacktest.tsx @@ -19,6 +19,7 @@ import { SignalPicker } from '@/components/screener/SignalPicker' import { startBacktest, stopBacktest, tryReconnect, useBacktestTask } from '@/lib/backtestTask' import { useDataStatus, useCapabilities } from '@/lib/useSharedQueries' import { EmptyState } from '@/components/EmptyState' +import { WarmupBadge } from '@/components/WarmupBadge' import { DatePicker } from '@/components/DatePicker' import { StrategyNavChart } from './charts/StrategyNavChart' import { ReturnDistributionChart } from './charts/ReturnDistributionChart' @@ -660,6 +661,7 @@ export function StrategyBacktest() { const [entryFill, setEntryFill] = useState<'close_t' | 'open_t+1'>(saved?.entryFill ?? saved?.matching ?? 'open_t+1') const [exitFill, setExitFill] = useState<'close_t' | 'open_t+1'>(saved?.exitFill ?? saved?.matching ?? 'close_t') const [fees, setFees] = useState(saved?.fees ?? '2') + const [slippage, setSlippage] = useState(saved?.slippage ?? '5') const [maxPositions, setMaxPositions] = useState(saved?.maxPositions ?? '10') const [maxExposure, setMaxExposure] = useState(saved?.maxExposure ?? '100') const [initialCapital, setInitialCapital] = useState(saved?.initialCapital ?? '1000000') @@ -765,6 +767,7 @@ export function StrategyBacktest() { entryFill, exitFill, fees, + slippage, maxPositions, maxExposure, initialCapital, @@ -789,6 +792,7 @@ export function StrategyBacktest() { entry_fill: entryFill, exit_fill: exitFill, fees_pct: Number(fees) / 10000, + slippage_bps: Number(slippage), max_positions: Number(maxPositions), max_exposure_pct: Number(maxExposure) / 100, initial_capital: Number(initialCapital), @@ -1141,7 +1145,10 @@ export function StrategyBacktest() {
-
回测区间
+
+
回测区间
+ +
{rangeTitle} @@ -1292,6 +1299,10 @@ export function StrategyBacktest() { setFees(e.target.value)} className={INPUT_CLS} />
+
+ + setSlippage(e.target.value)} className={INPUT_CLS} /> +
)} {simMode === 'position' && (