From 80d62aec563b356372d7aed07d30124086b2b3b4 Mon Sep 17 00:00:00 2001 From: Justin Gu <97915@qq.com> Date: Fri, 3 Jul 2026 04:54:04 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=9B=BE=E8=A1=A8x=E8=BD=B4=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E5=AE=8C=E6=95=B4=E6=97=A5=E6=9C=9F=20+=20=E7=BB=84?= =?UTF-8?q?=E5=90=88=E5=9B=9E=E6=B5=8B=E5=8F=96=E6=95=B0=E7=BF=BB=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 图表日期格式(4 个组件):KlineChart/EquityChart/CompareChart/ PortfolioCompareChart 的 xAxis 加 axisLabel.formatter,强制显示原始 日期字符串。ECharts 默认把 ISO 日期当时间对象解析,丢失年份只显示 月-日+00:00。 组合回测取数翻页(_fetch_portfolio_bars):固定 count=800 改为翻页循环 (与前端 fetchBars 同逻辑),拼接后 sort_values 排序再过滤。修复组合 回测图表从 800 根最早点(2023-03-14)开始、缺失早期数据的问题。 实测组合回测 2019-01-07 起:净值 1814 点(不再是 800),首日正确。 --- src/easy_tdx/web/routers/backtest.py | 43 +++++++++++++------ web-ui/src/components/CompareChart.vue | 7 ++- web-ui/src/components/EquityChart.vue | 1 + web-ui/src/components/KlineChart.vue | 5 +++ .../src/components/PortfolioCompareChart.vue | 7 ++- 5 files changed, 49 insertions(+), 14 deletions(-) diff --git a/src/easy_tdx/web/routers/backtest.py b/src/easy_tdx/web/routers/backtest.py index be25772..5f8357f 100644 --- a/src/easy_tdx/web/routers/backtest.py +++ b/src/easy_tdx/web/routers/backtest.py @@ -322,30 +322,49 @@ async def _fetch_portfolio_bars( ) -> list[Any]: """逐个标的取 K 线并组装 StockData 列表(async,必须在 event loop 内调用)。 - 单个标的取数失败时跳过(不中断整个组合),全部失败返回空列表。 + 当 start_date 超出单次 800 根覆盖范围时,自动翻页拉取(与前端 fetchBars + 同逻辑)。单个标的取数失败时跳过(不中断整个组合),全部失败返回空列表。 """ from easy_tdx.backtest.portfolio_engine import StockData from easy_tdx.web.convert import category_from_str, market_from_str + max_pages = 10 # 翻页上限:10 × 800 = 8000 根 stock_data_list: list[StockData] = [] for symbol in stocks: market_str, code = symbol.split(":", 1) - try: - df = await client.get_security_bars( - market_from_str(market_str), - code, - category_from_str(category), - 0, - 800, # 固定拉满,前端按日期过滤 - ) - except Exception: - continue # 单个标的失败不中断 - if len(df) < 2: + frames: list[pd.DataFrame] = [] + for page in range(max_pages): + try: + page_df = await client.get_security_bars( + market_from_str(market_str), + code, + category_from_str(category), + page * 800, + 800, + ) + except Exception: + break # 单页失败则停止该标的的翻页 + if len(page_df) == 0: + break + frames.append(page_df) + # 已覆盖到 start_date(本页最早一根 ≤ start_date)则停止 + if start_date and len(page_df) > 0: + dt_col = "datetime" if "datetime" in page_df.columns else "date" + oldest = str(page_df[dt_col].iloc[-1])[:10] + if oldest <= start_date: + break + if len(page_df) < 800: + break # 数据起点 + + if not frames: continue + df = pd.concat(frames, ignore_index=True) # 列名归一化:日线返回 date,分钟线返回 datetime if "datetime" not in df.columns and "date" in df.columns: df = df.copy() df["datetime"] = df["date"] + # 翻页拼接后按时间正序排序(页间逆序) + df = df.sort_values("datetime").reset_index(drop=True) # 日期范围过滤 if start_date or end_date: dt_str = df["datetime"].astype(str).str.slice(0, 10) diff --git a/web-ui/src/components/CompareChart.vue b/web-ui/src/components/CompareChart.vue index 6c50deb..5ff6618 100644 --- a/web-ui/src/components/CompareChart.vue +++ b/web-ui/src/components/CompareChart.vue @@ -41,7 +41,12 @@ function buildOption(): echarts.EChartsCoreOption { }, legend: { top: 0, data: seriesData.map((s) => s.name) }, grid: { left: '8%', right: '5%', top: 30, bottom: 50 }, - xAxis: { type: 'category', data: allDates, boundaryGap: false }, + xAxis: { + type: 'category', + data: allDates, + boundaryGap: false, + axisLabel: { formatter: (v: string) => v }, + }, yAxis: { type: 'value', scale: true, diff --git a/web-ui/src/components/EquityChart.vue b/web-ui/src/components/EquityChart.vue index 3c79192..fc1ce56 100644 --- a/web-ui/src/components/EquityChart.vue +++ b/web-ui/src/components/EquityChart.vue @@ -41,6 +41,7 @@ function buildOption(): echarts.EChartsCoreOption { data: dates, boundaryGap: false, axisLine: { onZero: false }, + axisLabel: { formatter: (v: string) => v }, }, yAxis: [ { diff --git a/web-ui/src/components/KlineChart.vue b/web-ui/src/components/KlineChart.vue index d1778f7..4ef026d 100644 --- a/web-ui/src/components/KlineChart.vue +++ b/web-ui/src/components/KlineChart.vue @@ -82,6 +82,11 @@ function buildOption(): echarts.EChartsCoreOption { boundaryGap: true, axisLine: { onZero: false }, splitLine: { show: false }, + // 强制显示原始日期字符串,避免 ECharts 把 "2024-01-15" 当时间对象 + // 解析后默认按 {MM}-{dd} {HH}:{mm} 显示(丢年份) + axisLabel: { + formatter: (v: string) => v, + }, }, yAxis: { scale: true, diff --git a/web-ui/src/components/PortfolioCompareChart.vue b/web-ui/src/components/PortfolioCompareChart.vue index 245c5cd..f799420 100644 --- a/web-ui/src/components/PortfolioCompareChart.vue +++ b/web-ui/src/components/PortfolioCompareChart.vue @@ -50,7 +50,12 @@ function buildOption( }, legend: { top: 0, data: series.map((s) => s.name) }, grid: { left: '8%', right: '5%', top: 30, bottom: 50 }, - xAxis: { type: 'category', data: allDates, boundaryGap: false }, + xAxis: { + type: 'category', + data: allDates, + boundaryGap: false, + axisLabel: { formatter: (v: string) => v }, + }, yAxis: { type: 'value', scale: true,